From 88e7c8bd356fc1fd892c7b7bb5159a8dc9cb8a98 Mon Sep 17 00:00:00 2001 From: Alex Quick Date: Sat, 6 May 2017 00:52:21 -0400 Subject: [PATCH 1/3] support for writing envs out in dotenv format --- godotenv.go | 39 +++++++++++++++++++++++++++++++++++++++ godotenv_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/godotenv.go b/godotenv.go index 269d7c7..26d2af9 100644 --- a/godotenv.go +++ b/godotenv.go @@ -16,6 +16,7 @@ package godotenv import ( "bufio" "errors" + "fmt" "io" "os" "os/exec" @@ -119,6 +120,11 @@ func Parse(r io.Reader) (envMap map[string]string, err error) { return } +//ParseString reads an env file from a string, returning a map of keys and values. +func ParseString(str string) (envMap map[string]string, err error) { + return Parse(strings.NewReader(str)) +} + // Exec loads env vars from the specified filenames (empty map falls back to default) // then executes the cmd specified. // @@ -136,6 +142,31 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error { return command.Run() } +// Write serializes the given environment and writes it to a file +func Write(envMap map[string]string, filename string) error { + content, error := WriteString(envMap) + if error != nil { + return error + } + file, error := os.Create(filename) + if error != nil { + return error + } + _, err := file.WriteString(content) + return err +} + +// WriteString outputs the given environment as a dotenv-formatted environment file. +// +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func WriteString(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + return strings.Join(lines, "\n"), nil +} + func filenamesOrDefault(filenames []string) []string { if len(filenames) == 0 { return []string{".env"} @@ -264,3 +295,11 @@ func isIgnoredLine(line string) bool { trimmedLine := strings.Trim(line, " \n\t") return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") } + +func doubleQuoteEscape(line string) string { + line = strings.Replace(line, `\`, `\\`, -1) + line = strings.Replace(line, "\n", `\n`, -1) + line = strings.Replace(line, "\r", `\r`, -1) + line = strings.Replace(line, `"`, `\"`, -1) + return line +} diff --git a/godotenv_test.go b/godotenv_test.go index 0bb5229..d554727 100644 --- a/godotenv_test.go +++ b/godotenv_test.go @@ -2,7 +2,9 @@ package godotenv import ( "bytes" + "fmt" "os" + "reflect" "testing" ) @@ -326,3 +328,47 @@ func TestErrorParsing(t *testing.T) { t.Errorf("Expected error, got %v", envMap) } } + +func TestWrite(t *testing.T) { + writeAndCompare := func(env string, expected string) { + envMap, _ := ParseString(env) + actual, _ := WriteString(envMap) + if expected != actual { + t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual) + } + } + //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 and backslashes are escaped + writeAndCompare(`foo="ba\n\r\\r!"`, `foo="ba\n\r\\r!"`) +} + +func TestRoundtrip(t *testing.T) { + fixtures := []string{"equals.env", "exported.env", "invalid1.env", "plain.env", "quoted.env"} + for _, fixture := range fixtures { + fixtureFilename := fmt.Sprintf("fixtures/%s", fixture) + env, err := readFile(fixtureFilename) + if err != nil { + continue + } + rep, err := WriteString(env) + if err != nil { + continue + } + roundtripped, err := ParseString(rep) + if err != nil { + continue + } + if !reflect.DeepEqual(env, roundtripped) { + t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped) + } + + } +} From 5d289f440502940d9fd8bed27b1c652fa5bca72d Mon Sep 17 00:00:00 2001 From: Alex Quick Date: Sun, 16 Jul 2017 18:31:51 -0400 Subject: [PATCH 2/3] escape some other bash-y special chars ($!) --- godotenv.go | 16 ++++++++++++---- godotenv_test.go | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/godotenv.go b/godotenv.go index 26d2af9..de2ff39 100644 --- a/godotenv.go +++ b/godotenv.go @@ -24,6 +24,8 @@ 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) @@ -297,9 +299,15 @@ func isIgnoredLine(line string) bool { } func doubleQuoteEscape(line string) string { - line = strings.Replace(line, `\`, `\\`, -1) - line = strings.Replace(line, "\n", `\n`, -1) - line = strings.Replace(line, "\r", `\r`, -1) - line = strings.Replace(line, `"`, `\"`, -1) + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } return line } diff --git a/godotenv_test.go b/godotenv_test.go index d554727..22e8f8b 100644 --- a/godotenv_test.go +++ b/godotenv_test.go @@ -346,8 +346,8 @@ func TestWrite(t *testing.T) { 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 and backslashes are escaped - writeAndCompare(`foo="ba\n\r\\r!"`, `foo="ba\n\r\\r!"`) + // newlines, backslashes, and some other special chars are escaped + writeAndCompare(`foo="$ba\n\r\\r!"`, `foo="\$ba\n\r\\r\!"`) } func TestRoundtrip(t *testing.T) { From b1bb9d9fc33d39f78baf8c246d09832eedb34119 Mon Sep 17 00:00:00 2001 From: Alex Quick Date: Wed, 13 Sep 2017 23:18:07 -0400 Subject: [PATCH 3/3] rename WriteString/ReadString to Marshal/Unmarshal --- godotenv.go | 11 +++++------ godotenv_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/godotenv.go b/godotenv.go index de2ff39..2710572 100644 --- a/godotenv.go +++ b/godotenv.go @@ -122,8 +122,8 @@ func Parse(r io.Reader) (envMap map[string]string, err error) { return } -//ParseString reads an env file from a string, returning a map of keys and values. -func ParseString(str string) (envMap map[string]string, err error) { +//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)) } @@ -146,7 +146,7 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error { // Write serializes the given environment and writes it to a file func Write(envMap map[string]string, filename string) error { - content, error := WriteString(envMap) + content, error := Marshal(envMap) if error != nil { return error } @@ -158,10 +158,9 @@ func Write(envMap map[string]string, filename string) error { return err } -// WriteString outputs the given environment as a dotenv-formatted environment file. -// +// Marshal outputs the given environment as a dotenv-formatted environment file. // Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. -func WriteString(envMap map[string]string) (string, error) { +func Marshal(envMap map[string]string) (string, error) { lines := make([]string, 0, len(envMap)) for k, v := range envMap { lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) diff --git a/godotenv_test.go b/godotenv_test.go index 22e8f8b..47b0c35 100644 --- a/godotenv_test.go +++ b/godotenv_test.go @@ -331,8 +331,8 @@ func TestErrorParsing(t *testing.T) { func TestWrite(t *testing.T) { writeAndCompare := func(env string, expected string) { - envMap, _ := ParseString(env) - actual, _ := WriteString(envMap) + 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) } @@ -358,11 +358,11 @@ func TestRoundtrip(t *testing.T) { if err != nil { continue } - rep, err := WriteString(env) + rep, err := Marshal(env) if err != nil { continue } - roundtripped, err := ParseString(rep) + roundtripped, err := Unmarshal(rep) if err != nil { continue }