Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Options.EscapeExclamation = false #173

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions godotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import (
"strings"
)

const doubleQuoteSpecialChars = "\\\n\r\"!$`"

// Load will read your env file(s) and load them into ENV for this process.
//
// Call this function as close as possible to the start of your program (ideally in main)
Expand Down Expand Up @@ -124,7 +122,7 @@ func Parse(r io.Reader) (envMap map[string]string, err error) {
return
}

//Unmarshal reads an env file from a string, returning a map of keys and values.
// Unmarshal reads an env file from a string, returning a map of keys and values.
func Unmarshal(str string) (envMap map[string]string, err error) {
return Parse(strings.NewReader(str))
}
Expand Down Expand Up @@ -254,7 +252,7 @@ func parseLine(line string, envMap map[string]string) (key string, value string,
firstColon := strings.Index(line, ":")
splitString := strings.SplitN(line, "=", 2)
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
//this is a yaml-style line
// this is a yaml-style line
splitString = strings.SplitN(line, ":", 2)
}

Expand Down Expand Up @@ -285,7 +283,6 @@ var (
)

func parseValue(value string, envMap map[string]string) string {

// trim
value = strings.Trim(value, " ")

Expand Down Expand Up @@ -348,8 +345,16 @@ func isIgnoredLine(line string) bool {
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
}

func getDoubleQuoteSpecialChars() string {
result := "\\\n\r\"$`"
if Options.EscapeExclamation {
return result + "!"
}
return result
}

func doubleQuoteEscape(line string) string {
for _, c := range doubleQuoteSpecialChars {
for _, c := range getDoubleQuoteSpecialChars() {
toReplace := "\\" + string(c)
if c == '\n' {
toReplace = `\n`
Expand Down
81 changes: 57 additions & 24 deletions godotenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ func TestExpanding(t *testing.T) {
}
})
}

}

func TestActualEnvVarsAreLeftAlone(t *testing.T) {
Expand Down Expand Up @@ -304,7 +303,7 @@ func TestParsing(t *testing.T) {
// parses yaml style options
parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")

//parses yaml values with equal signs
// parses yaml values with equal signs
parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar")

// parses non-yaml options with colons
Expand Down Expand Up @@ -353,7 +352,7 @@ func TestParsing(t *testing.T) {
parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")

//newlines and backslashes should be escaped
// newlines and backslashes should be escaped
parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz")
parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz")
parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz")
Expand Down Expand Up @@ -425,29 +424,63 @@ func TestErrorParsing(t *testing.T) {
}

func TestWrite(t *testing.T) {
writeAndCompare := func(env string, expected string) {
envMap, _ := Unmarshal(env)
actual, _ := Marshal(envMap)
if expected != actual {
t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual)
}
testCases := []struct {
desc string
value string
expected string
dontEscapeExclamation bool
}{
{
desc: "values are always double-quoted",
value: `key=value`,
expected: `key="value"`,
},
{
desc: "double-quotes are escaped",
value: `key=va"lu"e`,
expected: `key="va\"lu\"e"`,
},
{
desc: "but single quotes are left alone",
value: `key=va'lu'e`,
expected: `key="va'lu'e"`,
},
{
desc: "newlines, backslashes, and some other special chars are escaped",
value: `foo="\n\r\\r!"`,
expected: `foo="\n\r\\r\!"`,
},
{
desc: "but when EscapeExclamation=false, should not escape exclamation",
value: `foo="\n\r\\r!"`,
expected: `foo="\n\r\\r!"`,
dontEscapeExclamation: true,
},
{
desc: "lines should be sorted",
value: "foo=bar\nbaz=buzz",
expected: "baz=\"buzz\"\nfoo=\"bar\"",
},
{
desc: "integers should not be quoted",
value: `key="10"`,
expected: `key=10`,
},
}
//just test some single lines to show the general idea
//TestRoundtrip makes most of the good assertions

//values are always double-quoted
writeAndCompare(`key=value`, `key="value"`)
//double-quotes are escaped
writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
//but single quotes are left alone
writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
// newlines, backslashes, and some other special chars are escaped
writeAndCompare(`foo="\n\r\\r!"`, `foo="\n\r\\r\!"`)
// lines should be sorted
writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")
// integers should not be quoted
writeAndCompare(`key="10"`, `key=10`)
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
if tc.dontEscapeExclamation {
Options.EscapeExclamation = false
}

envMap, _ := Unmarshal(tc.value)
actual, _ := Marshal(envMap)

if tc.expected != actual {
t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", tc.value, envMap, tc.expected, actual)
}
})
}
}

func TestRoundtrip(t *testing.T) {
Expand Down
11 changes: 11 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package godotenv

// GoDotEnvOptions stores options for the parser
type GoDotEnvOptions struct {
EscapeExclamation bool
}

// default options
var Options = GoDotEnvOptions{
EscapeExclamation: true,
}