1package godotenv
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "regexp"
8 "strings"
9 "unicode"
10)
11
12const (
13 charComment = '#'
14 prefixSingleQuote = '\''
15 prefixDoubleQuote = '"'
16
17 exportPrefix = "export"
18)
19
20func parseBytes(src []byte, out map[string]string) error {
21 src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)
22 cutset := src
23 for {
24 cutset = getStatementStart(cutset)
25 if cutset == nil {
26 // reached end of file
27 break
28 }
29
30 key, left, err := locateKeyName(cutset)
31 if err != nil {
32 return err
33 }
34
35 value, left, err := extractVarValue(left, out)
36 if err != nil {
37 return err
38 }
39
40 out[key] = value
41 cutset = left
42 }
43
44 return nil
45}
46
47// getStatementPosition returns position of statement begin.
48//
49// It skips any comment line or non-whitespace character.
50func getStatementStart(src []byte) []byte {
51 pos := indexOfNonSpaceChar(src)
52 if pos == -1 {
53 return nil
54 }
55
56 src = src[pos:]
57 if src[0] != charComment {
58 return src
59 }
60
61 // skip comment section
62 pos = bytes.IndexFunc(src, isCharFunc('\n'))
63 if pos == -1 {
64 return nil
65 }
66
67 return getStatementStart(src[pos:])
68}
69
70// locateKeyName locates and parses key name and returns rest of slice
71func locateKeyName(src []byte) (key string, cutset []byte, err error) {
72 // trim "export" and space at beginning
73 src = bytes.TrimLeftFunc(src, isSpace)
74 if bytes.HasPrefix(src, []byte(exportPrefix)) {
75 trimmed := bytes.TrimPrefix(src, []byte(exportPrefix))
76 if bytes.IndexFunc(trimmed, isSpace) == 0 {
77 src = bytes.TrimLeftFunc(trimmed, isSpace)
78 }
79 }
80
81 // locate key name end and validate it in single loop
82 offset := 0
83loop:
84 for i, char := range src {
85 rchar := rune(char)
86 if isSpace(rchar) {
87 continue
88 }
89
90 switch char {
91 case '=', ':':
92 // library also supports yaml-style value declaration
93 key = string(src[0:i])
94 offset = i + 1
95 break loop
96 case '_':
97 default:
98 // variable name should match [A-Za-z0-9_.]
99 if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' {
100 continue
101 }
102
103 return "", nil, fmt.Errorf(
104 `unexpected character %q in variable name near %q`,
105 string(char), string(src))
106 }
107 }
108
109 if len(src) == 0 {
110 return "", nil, errors.New("zero length string")
111 }
112
113 // trim whitespace
114 key = strings.TrimRightFunc(key, unicode.IsSpace)
115 cutset = bytes.TrimLeftFunc(src[offset:], isSpace)
116 return key, cutset, nil
117}
118
119// extractVarValue extracts variable value and returns rest of slice
120func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) {
121 quote, hasPrefix := hasQuotePrefix(src)
122 if !hasPrefix {
123 // unquoted value - read until end of line
124 endOfLine := bytes.IndexFunc(src, isLineEnd)
125
126 // Hit EOF without a trailing newline
127 if endOfLine == -1 {
128 endOfLine = len(src)
129
130 if endOfLine == 0 {
131 return "", nil, nil
132 }
133 }
134
135 // Convert line to rune away to do accurate countback of runes
136 line := []rune(string(src[0:endOfLine]))
137
138 // Assume end of line is end of var
139 endOfVar := len(line)
140 if endOfVar == 0 {
141 return "", src[endOfLine:], nil
142 }
143
144 // Work backwards to check if the line ends in whitespace then
145 // a comment (ie asdasd # some comment)
146 for i := endOfVar - 1; i >= 0; i-- {
147 if line[i] == charComment && i > 0 {
148 if isSpace(line[i-1]) {
149 endOfVar = i
150 break
151 }
152 }
153 }
154
155 trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace)
156
157 return expandVariables(trimmed, vars), src[endOfLine:], nil
158 }
159
160 // lookup quoted string terminator
161 for i := 1; i < len(src); i++ {
162 if char := src[i]; char != quote {
163 continue
164 }
165
166 // skip escaped quote symbol (\" or \', depends on quote)
167 if prevChar := src[i-1]; prevChar == '\\' {
168 continue
169 }
170
171 // trim quotes
172 trimFunc := isCharFunc(rune(quote))
173 value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
174 if quote == prefixDoubleQuote {
175 // unescape newlines for double quote (this is compat feature)
176 // and expand environment variables
177 value = expandVariables(expandEscapes(value), vars)
178 }
179
180 return value, src[i+1:], nil
181 }
182
183 // return formatted error if quoted string is not terminated
184 valEndIndex := bytes.IndexFunc(src, isCharFunc('\n'))
185 if valEndIndex == -1 {
186 valEndIndex = len(src)
187 }
188
189 return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
190}
191
192func expandEscapes(str string) string {
193 out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string {
194 c := strings.TrimPrefix(match, `\`)
195 switch c {
196 case "n":
197 return "\n"
198 case "r":
199 return "\r"
200 default:
201 return match
202 }
203 })
204 return unescapeCharsRegex.ReplaceAllString(out, "$1")
205}
206
207func indexOfNonSpaceChar(src []byte) int {
208 return bytes.IndexFunc(src, func(r rune) bool {
209 return !unicode.IsSpace(r)
210 })
211}
212
213// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
214func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) {
215 if len(src) == 0 {
216 return 0, false
217 }
218
219 switch prefix := src[0]; prefix {
220 case prefixDoubleQuote, prefixSingleQuote:
221 return prefix, true
222 default:
223 return 0, false
224 }
225}
226
227func isCharFunc(char rune) func(rune) bool {
228 return func(v rune) bool {
229 return v == char
230 }
231}
232
233// isSpace reports whether the rune is a space character but not line break character
234//
235// this differs from unicode.IsSpace, which also applies line break as space
236func isSpace(r rune) bool {
237 switch r {
238 case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0:
239 return true
240 }
241 return false
242}
243
244func isLineEnd(r rune) bool {
245 if r == '\n' || r == '\r' {
246 return true
247 }
248 return false
249}
250
251var (
252 escapeRegex = regexp.MustCompile(`\\.`)
253 expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
254 unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
255)
256
257func expandVariables(v string, m map[string]string) string {
258 return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
259 submatch := expandVarRegex.FindStringSubmatch(s)
260
261 if submatch == nil {
262 return s
263 }
264 if submatch[1] == "\\" || submatch[2] == "(" {
265 return submatch[0][1:]
266 } else if submatch[4] != "" {
267 return m[submatch[4]]
268 }
269 return s
270 })
271}