11// Package envparse is a fork of the github.com/joho/godotenv parser.
22//
3+ // This fork is based on master which has some minor fixes[1] since the v1.5.1
4+ // we previously used.
5+ //
6+ // [1] https://github.com/joho/godotenv/compare/v1.5.1...main
7+ //
38// -------
49//
510// # Copyright (c) 2013 John Barton
@@ -30,12 +35,39 @@ import (
3035 "bytes"
3136 "errors"
3237 "fmt"
33- "os"
34- "regexp"
38+ "io"
3539 "strings"
3640 "unicode"
3741)
3842
43+ // Parse reads an env file from io.Reader, returning a map of keys and values.
44+ func Parse (r io.Reader ) (map [string ]string , error ) {
45+ data , err := io .ReadAll (r )
46+ if err != nil {
47+ return nil , err
48+ }
49+ return ParseData (data )
50+ }
51+
52+ // ParseData is like Parse but works on a string or []byte slice.
53+ func ParseData [T ~ string | ~ []byte ](data T ) (map [string ]string , error ) {
54+ buf := []byte (data )
55+ if _ , ok := any (data ).([]byte ); ok {
56+ // since we use data as scratch space
57+ buf = bytes .Clone (buf )
58+ }
59+ return parseData (buf )
60+ }
61+
62+ // parseData will mutate data during parsing, use ParseData to avoid this.
63+ func parseData (data []byte ) (map [string ]string , error ) {
64+ out := make (map [string ]string )
65+ if err := parseBytes ([]byte (data ), out ); err != nil {
66+ return nil , err
67+ }
68+ return out , nil
69+ }
70+
3971const (
4072 charComment = '#'
4173 prefixSingleQuote = '\''
@@ -59,7 +91,7 @@ func parseBytes(src []byte, out map[string]string) error {
5991 return err
6092 }
6193
62- value , left , err := extractVarValue (left , out )
94+ value , left , err := extractVarValue (left )
6395 if err != nil {
6496 return err
6597 }
@@ -143,8 +175,8 @@ loop:
143175 return key , cutset , nil
144176}
145177
146- // extractVarValue extracts variable value and returns rest of slice
147- func extractVarValue (src []byte , vars map [ string ] string ) (value string , rest []byte , err error ) {
178+ // extractVarValue extracts variable value and returns rest of slice.
179+ func extractVarValue (src []byte ) (value string , rest []byte , err error ) {
148180 quote , hasPrefix := hasQuotePrefix (src )
149181 if ! hasPrefix {
150182 // unquoted value - read until end of line
@@ -168,42 +200,36 @@ func extractVarValue(src []byte, vars map[string]string) (value string, rest []b
168200 return "" , src [endOfLine :], nil
169201 }
170202
171- // Work backwards to check if the line ends in whitespace then
172- // a comment, ie: foo =bar # baz # other
173- for i := 0 ; i < endOfVar ; i ++ {
174- if line [ i ] == charComment && i < endOfVar {
175- if isSpace ( line [ i - 1 ]) {
176- endOfVar = i
177- break
178- }
203+ // Strip trailing comments only when '#' is preceded by whitespace:
204+ // FOO =bar # comment => "bar"
205+ // FOO=bar#baz => "bar#baz"
206+ // FOO=#bar => "#bar"
207+ for i := 1 ; i < endOfVar ; i ++ {
208+ if line [ i ] == charComment && isSpace ( line [ i - 1 ]) {
209+ endOfVar = i
210+ break
179211 }
180212 }
181213
182214 trimmed := strings .TrimFunc (string (line [0 :endOfVar ]), isSpace )
183-
184- return expandVariables (trimmed , vars ), src [endOfLine :], nil
215+ return trimmed , src [endOfLine :], nil
185216 }
186217
187218 // lookup quoted string terminator
188219 for i := 1 ; i < len (src ); i ++ {
189- if char := src [i ]; char != quote {
220+ if src [i ] != quote {
190221 continue
191222 }
192-
193- // skip escaped quote symbol (\" or \', depends on quote)
194- if prevChar := src [i - 1 ]; prevChar == '\\' {
223+ if isEscaped (src , i ) {
195224 continue
196225 }
197226
198- // trim quotes
199- trimFunc := isCharFunc (rune (quote ))
200- value = string (bytes .TrimLeftFunc (bytes .TrimRightFunc (src [0 :i ], trimFunc ), trimFunc ))
227+ valueBytes := src [1 :i ]
201228 if quote == prefixDoubleQuote {
202- // unescape newlines for double quote (this is compat feature)
203- // and expand environment variables
204- value = expandVariables (expandEscapes (value ), vars )
229+ valueBytes = expandEscapes (valueBytes )
205230 }
206231
232+ value = string (valueBytes )
207233 return value , src [i + 1 :], nil
208234 }
209235
@@ -212,23 +238,45 @@ func extractVarValue(src []byte, vars map[string]string) (value string, rest []b
212238 if valEndIndex == - 1 {
213239 valEndIndex = len (src )
214240 }
215-
216241 return "" , nil , fmt .Errorf ("unterminated quoted value %s" , src [:valEndIndex ])
217242}
218243
219- func expandEscapes (str string ) string {
220- out := escapeRegex .ReplaceAllStringFunc (str , func (match string ) string {
221- c := strings .TrimPrefix (match , `\` )
222- switch c {
223- case "n" :
224- return "\n "
225- case "r" :
226- return "\r "
244+ func isEscaped (src []byte , index int ) bool {
245+ var n int
246+ for i := index - 1 ; i >= 0 && src [i ] == '\\' ; i -- {
247+ n ++
248+ }
249+ return n % 2 == 1
250+ }
251+
252+ func expandEscapes (src []byte ) []byte {
253+ var n int
254+ for r := 0 ; r < len (src ); r ++ {
255+ if src [r ] != '\\' || r + 1 >= len (src ) {
256+ src [n ] = src [r ]
257+ n ++
258+ continue
259+ }
260+
261+ r ++
262+ switch src [r ] {
263+ case 'n' :
264+ src [n ] = '\n'
265+ case 'r' :
266+ src [n ] = '\r'
267+ case '$' :
268+ // TODO(cstockton): We keep '$' here for stricter compat with todays
269+ // config. If we want to be more strict (e.g. \$ -> \$) we can emit
270+ // the additional \\ as well.
271+ src [n ] = '$'
227272 default :
228- return match
273+ // Preserve upstream godotenv behavior for non-dollar escapes:
274+ // \" => ", \\ => \, \x => x.
275+ src [n ] = src [r ]
229276 }
230- })
231- return unescapeCharsRegex .ReplaceAllString (out , "$1" )
277+ n ++
278+ }
279+ return src [:n ]
232280}
233281
234282func indexOfNonSpaceChar (src []byte ) int {
@@ -274,31 +322,3 @@ func isLineEnd(r rune) bool {
274322 }
275323 return false
276324}
277-
278- var (
279- escapeRegex = regexp .MustCompile (`\\.` )
280- expandVarRegex = regexp .MustCompile (`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?` )
281- unescapeCharsRegex = regexp .MustCompile (`\\([^$])` )
282- )
283-
284- func expandVariables (v string , m map [string ]string ) string {
285- return expandVarRegex .ReplaceAllStringFunc (v , func (s string ) string {
286- submatch := expandVarRegex .FindStringSubmatch (s )
287-
288- if submatch == nil {
289- return s
290- }
291- if submatch [1 ] == "\\ " || submatch [2 ] == "(" {
292- return submatch [0 ][1 :]
293- } else if submatch [4 ] != "" {
294- if val , ok := m [submatch [4 ]]; ok {
295- return val
296- }
297- if val , ok := os .LookupEnv (submatch [4 ]); ok {
298- return val
299- }
300- return m [submatch [4 ]]
301- }
302- return s
303- })
304- }
0 commit comments