From 6f7c94fd501911de9073bd510f19337014adffb9 Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Sat, 21 May 2022 18:22:12 +0100 Subject: [PATCH 1/5] refactor: small cleanup --- gotenv.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gotenv.go b/gotenv.go index 7ffc06b..fb4bcb1 100644 --- a/gotenv.go +++ b/gotenv.go @@ -16,6 +16,9 @@ const ( // Pattern for detecting valid variable within a value variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)` + + // Byte order mark character + bom = "\xef\xbb\xbf" ) // Env holds key/value pair of valid environment variable @@ -125,18 +128,16 @@ func strictParse(r io.Reader, override bool) (Env, error) { env := make(Env) scanner := bufio.NewScanner(r) - i := 1 - bom := string([]byte{239, 187, 191}) + firstLine := true for scanner.Scan() { line := scanner.Text() - if i == 1 { + if firstLine { line = strings.TrimPrefix(line, bom) + firstLine = false } - i++ - err := parseLine(line, env, override) if err != nil { return env, err @@ -170,8 +171,8 @@ func parseLine(s string, env Env, override bool) error { val = rq.ReplaceAllString(val, "$2") if hdq { - val = strings.Replace(val, `\n`, "\n", -1) - val = strings.Replace(val, `\r`, "\r", -1) + val = strings.ReplaceAll(val, `\n`, "\n") + val = strings.ReplaceAll(val, `\r`, "\r") // Unescape all characters except $ so variables can be escaped properly re := regexp.MustCompile(`\\([^$])`) @@ -255,9 +256,8 @@ func parseVal(val string, env Env, ignoreNewlines bool, override bool) string { if len(kv) > 1 { val = kv[0] - - for i := 1; i < len(kv); i++ { - parseLine(kv[i], env, override) + for _, l := range kv[1:] { + _ = parseLine(l, env, override) } } } From 534e60cfb6b10c3e2a26e6b322bb2c6e0bb115fc Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Sat, 21 May 2022 18:57:37 +0100 Subject: [PATCH 2/5] refactor: move regexp.MustCompile to globals --- gotenv.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/gotenv.go b/gotenv.go index fb4bcb1..c471f34 100644 --- a/gotenv.go +++ b/gotenv.go @@ -146,9 +146,16 @@ func strictParse(r io.Reader, override bool) (Env, error) { return env, nil } + +var ( + lineRgx = regexp.MustCompile(linePattern) + quotesRgx = regexp.MustCompile(`\A(['"])(.*)(['"])\z`) + unescapeRgx = regexp.MustCompile(`\\([^$])`) + varRgx = regexp.MustCompile(variablePattern) +) + func parseLine(s string, env Env, override bool) error { - rl := regexp.MustCompile(linePattern) - rm := rl.FindStringSubmatch(s) + rm := lineRgx.FindStringSubmatch(s) if len(rm) == 0 { return checkFormat(s, env) @@ -167,24 +174,21 @@ func parseLine(s string, env Env, override bool) error { val = strings.Trim(val, " ") // remove quotes '' or "" - rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`) - val = rq.ReplaceAllString(val, "$2") + val = quotesRgx.ReplaceAllString(val, "$2") if hdq { val = strings.ReplaceAll(val, `\n`, "\n") val = strings.ReplaceAll(val, `\r`, "\r") // Unescape all characters except $ so variables can be escaped properly - re := regexp.MustCompile(`\\([^$])`) - val = re.ReplaceAllString(val, "$1") + val = unescapeRgx.ReplaceAllString(val, "$1") } - rv := regexp.MustCompile(variablePattern) fv := func(s string) string { return varReplacement(s, hsq, env, override) } - val = rv.ReplaceAllStringFunc(val, fv) + val = varRgx.ReplaceAllStringFunc(val, fv) val = parseVal(val, env, hdq, override) env[key] = val @@ -205,6 +209,8 @@ func parseExport(st string, env Env) error { return nil } +var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`) + func varReplacement(s string, hsq bool, env Env, override bool) string { if strings.HasPrefix(s, "\\") { return strings.TrimPrefix(s, "\\") @@ -214,9 +220,7 @@ func varReplacement(s string, hsq bool, env Env, override bool) string { return s } - sn := `(\$)(\{?([A-Z0-9_]+)\}?)` - rn := regexp.MustCompile(sn) - mn := rn.FindStringSubmatch(s) + mn := varNameRgx.FindStringSubmatch(s) if len(mn) == 0 { return s From 2c6c73744acba8db1259892c4e1f348ed35e6524 Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Sat, 21 May 2022 20:37:03 +0100 Subject: [PATCH 3/5] feat: support multi-line values Signed-off-by: Luis Davim --- fixtures/exported.env | 23 +++++++++++++++++++++++ fixtures/quoted.env | 2 ++ gotenv.go | 40 +++++++++++++++++++++++++++++++++++++--- gotenv_test.go | 23 +++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/fixtures/exported.env b/fixtures/exported.env index 5821377..c3ef9aa 100644 --- a/fixtures/exported.env +++ b/fixtures/exported.env @@ -1,2 +1,25 @@ export OPTION_A=2 export OPTION_B='\n' +# This is a comment +export OPTION_C='The MIT License (MIT) + +Copyright (c) 2013 Alif Rachmawadi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.' + diff --git a/fixtures/quoted.env b/fixtures/quoted.env index a03ce24..ce77cfb 100644 --- a/fixtures/quoted.env +++ b/fixtures/quoted.env @@ -6,3 +6,5 @@ OPTION_E="1" OPTION_F="2" OPTION_G="" OPTION_H="\n" +OPTION_I="some multi-line text +with \"escaped quotes\" and ${OPTION_A} variable" diff --git a/gotenv.go b/gotenv.go index c471f34..0d0e2d5 100644 --- a/gotenv.go +++ b/gotenv.go @@ -131,13 +131,48 @@ func strictParse(r io.Reader, override bool) (Env, error) { firstLine := true for scanner.Scan() { - line := scanner.Text() + line := strings.TrimSpace(scanner.Text()) if firstLine { line = strings.TrimPrefix(line, bom) firstLine = false } + if line == "" || line[0] == '#' { + continue + } + + quote := "" + idx := strings.Index(line, "=") + if idx == -1 { + idx = strings.Index(line, ":") + } + if idx > 0 && idx < len(line)-1 { + val := strings.TrimSpace(line[idx+1:]) + if val[0] == '"' || val[0] == '\'' { + quote = val[:1] + idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote) + if idx >= 0 && val[idx] != '\\' { + quote = "" + } + } + } + for quote != "" && scanner.Scan() { + l := scanner.Text() + line += "\n" + l + idx := strings.LastIndex(l, quote) + if idx > 0 && l[idx-1] == '\\' { + continue + } + if idx >= 0 { + quote = "" + } + } + + if quote != "" { + return env, fmt.Errorf("missing quotes") + } + err := parseLine(line, env, override) if err != nil { return env, err @@ -149,7 +184,6 @@ func strictParse(r io.Reader, override bool) (Env, error) { var ( lineRgx = regexp.MustCompile(linePattern) - quotesRgx = regexp.MustCompile(`\A(['"])(.*)(['"])\z`) unescapeRgx = regexp.MustCompile(`\\([^$])`) varRgx = regexp.MustCompile(variablePattern) ) @@ -174,7 +208,7 @@ func parseLine(s string, env Env, override bool) error { val = strings.Trim(val, " ") // remove quotes '' or "" - val = quotesRgx.ReplaceAllString(val, "$2") + val = strings.Trim(val, `'"`) if hdq { val = strings.ReplaceAll(val, `\n`, "\n") diff --git a/gotenv_test.go b/gotenv_test.go index 26a5eb4..21c7148 100644 --- a/gotenv_test.go +++ b/gotenv_test.go @@ -153,6 +153,27 @@ var fixtures = []struct { gotenv.Env{ "OPTION_A": "2", "OPTION_B": `\n`, + "OPTION_C": `The MIT License (MIT) + +Copyright (c) 2013 Alif Rachmawadi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.`, }, }, { @@ -176,6 +197,8 @@ var fixtures = []struct { "OPTION_F": "2", "OPTION_G": "", "OPTION_H": "\n", + "OPTION_I": `some multi-line text +with "escaped quotes" and 1 variable`, }, }, { From 36b11c3994e9bed2b420f6784f4c67fb08c898ea Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Sat, 21 May 2022 23:44:29 +0100 Subject: [PATCH 4/5] fix: do not parse single quoted values --- fixtures/quoted.env | 1 + gotenv.go | 16 ++++++++++------ gotenv_test.go | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/fixtures/quoted.env b/fixtures/quoted.env index ce77cfb..1092b76 100644 --- a/fixtures/quoted.env +++ b/fixtures/quoted.env @@ -8,3 +8,4 @@ OPTION_G="" OPTION_H="\n" OPTION_I="some multi-line text with \"escaped quotes\" and ${OPTION_A} variable" +OPTION_J='some$pecial$1$2!*chars=qweq""e$$\$""' diff --git a/gotenv.go b/gotenv.go index 0d0e2d5..c4c1e50 100644 --- a/gotenv.go +++ b/gotenv.go @@ -198,17 +198,19 @@ func parseLine(s string, env Env, override bool) error { key := rm[1] val := rm[2] + // trim whitespace + val = strings.TrimSpace(val) + // determine if string has quote prefix hdq := strings.HasPrefix(val, `"`) // determine if string has single quote prefix hsq := strings.HasPrefix(val, `'`) - // trim whitespace - val = strings.Trim(val, " ") - // remove quotes '' or "" - val = strings.Trim(val, `'"`) + if l := len(val); (hsq || hdq) && l >= 2 { + val = val[1 : l-1] + } if hdq { val = strings.ReplaceAll(val, `\n`, "\n") @@ -222,8 +224,10 @@ func parseLine(s string, env Env, override bool) error { return varReplacement(s, hsq, env, override) } - val = varRgx.ReplaceAllStringFunc(val, fv) - val = parseVal(val, env, hdq, override) + if !hsq { + val = varRgx.ReplaceAllStringFunc(val, fv) + val = parseVal(val, env, hdq, override) + } env[key] = val return nil diff --git a/gotenv_test.go b/gotenv_test.go index 21c7148..4c08df1 100644 --- a/gotenv_test.go +++ b/gotenv_test.go @@ -199,6 +199,7 @@ THE SOFTWARE.`, "OPTION_H": "\n", "OPTION_I": `some multi-line text with "escaped quotes" and 1 variable`, + "OPTION_J": `some$pecial$1$2!*chars=qweq""e$$\$""`, }, }, { From cb62e96f9c0bf18321525a1f70cb20b1b234977f Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Sat, 21 May 2022 23:49:44 +0100 Subject: [PATCH 5/5] chore: update go version and dependencies --- go.mod | 10 ++++++++-- go.sum | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f99ba49..42efe29 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,11 @@ module github.com/subosito/gotenv -go 1.13 +go 1.18 -require github.com/stretchr/testify v1.4.0 +require github.com/stretchr/testify v1.7.0 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index 8fdee58..acb88a4 100644 --- a/go.sum +++ b/go.sum @@ -3,9 +3,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=