Skip to content

Commit

Permalink
Merge pull request #35 from alexquick/feature-write-dotenv
Browse files Browse the repository at this point in the history
support for writing envs out in dotenv format
  • Loading branch information
joho authored Sep 14, 2017
2 parents c9360df + b1bb9d9 commit 9739509
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 0 deletions.
46 changes: 46 additions & 0 deletions godotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ package godotenv
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"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 @@ -119,6 +122,11 @@ 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.
func Unmarshal(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.
//
Expand All @@ -136,6 +144,30 @@ 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 := Marshal(envMap)
if error != nil {
return error
}
file, error := os.Create(filename)
if error != nil {
return error
}
_, err := file.WriteString(content)
return err
}

// 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 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)))
}
return strings.Join(lines, "\n"), nil
}

func filenamesOrDefault(filenames []string) []string {
if len(filenames) == 0 {
return []string{".env"}
Expand Down Expand Up @@ -264,3 +296,17 @@ func isIgnoredLine(line string) bool {
trimmedLine := strings.Trim(line, " \n\t")
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
}

func doubleQuoteEscape(line string) string {
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
}
46 changes: 46 additions & 0 deletions godotenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package godotenv

import (
"bytes"
"fmt"
"os"
"reflect"
"testing"
)

Expand Down Expand Up @@ -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, _ := Unmarshal(env)
actual, _ := Marshal(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, backslashes, and some other special chars 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 := Marshal(env)
if err != nil {
continue
}
roundtripped, err := Unmarshal(rep)
if err != nil {
continue
}
if !reflect.DeepEqual(env, roundtripped) {
t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
}

}
}

0 comments on commit 9739509

Please sign in to comment.