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) + } + + } +}