Skip to content
This repository was archived by the owner on Jan 12, 2022. It is now read-only.

Commit 9739509

Browse files
authored
Merge pull request joho#35 from alexquick/feature-write-dotenv
support for writing envs out in dotenv format
2 parents c9360df + b1bb9d9 commit 9739509

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

godotenv.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ package godotenv
1616
import (
1717
"bufio"
1818
"errors"
19+
"fmt"
1920
"io"
2021
"os"
2122
"os/exec"
2223
"regexp"
2324
"strings"
2425
)
2526

27+
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
28+
2629
// Load will read your env file(s) and load them into ENV for this process.
2730
//
2831
// Call this function as close as possible to the start of your program (ideally in main)
@@ -119,6 +122,11 @@ func Parse(r io.Reader) (envMap map[string]string, err error) {
119122
return
120123
}
121124

125+
//Unmarshal reads an env file from a string, returning a map of keys and values.
126+
func Unmarshal(str string) (envMap map[string]string, err error) {
127+
return Parse(strings.NewReader(str))
128+
}
129+
122130
// Exec loads env vars from the specified filenames (empty map falls back to default)
123131
// then executes the cmd specified.
124132
//
@@ -136,6 +144,30 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error {
136144
return command.Run()
137145
}
138146

147+
// Write serializes the given environment and writes it to a file
148+
func Write(envMap map[string]string, filename string) error {
149+
content, error := Marshal(envMap)
150+
if error != nil {
151+
return error
152+
}
153+
file, error := os.Create(filename)
154+
if error != nil {
155+
return error
156+
}
157+
_, err := file.WriteString(content)
158+
return err
159+
}
160+
161+
// Marshal outputs the given environment as a dotenv-formatted environment file.
162+
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
163+
func Marshal(envMap map[string]string) (string, error) {
164+
lines := make([]string, 0, len(envMap))
165+
for k, v := range envMap {
166+
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
167+
}
168+
return strings.Join(lines, "\n"), nil
169+
}
170+
139171
func filenamesOrDefault(filenames []string) []string {
140172
if len(filenames) == 0 {
141173
return []string{".env"}
@@ -264,3 +296,17 @@ func isIgnoredLine(line string) bool {
264296
trimmedLine := strings.Trim(line, " \n\t")
265297
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
266298
}
299+
300+
func doubleQuoteEscape(line string) string {
301+
for _, c := range doubleQuoteSpecialChars {
302+
toReplace := "\\" + string(c)
303+
if c == '\n' {
304+
toReplace = `\n`
305+
}
306+
if c == '\r' {
307+
toReplace = `\r`
308+
}
309+
line = strings.Replace(line, string(c), toReplace, -1)
310+
}
311+
return line
312+
}

godotenv_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package godotenv
22

33
import (
44
"bytes"
5+
"fmt"
56
"os"
7+
"reflect"
68
"testing"
79
)
810

@@ -326,3 +328,47 @@ func TestErrorParsing(t *testing.T) {
326328
t.Errorf("Expected error, got %v", envMap)
327329
}
328330
}
331+
332+
func TestWrite(t *testing.T) {
333+
writeAndCompare := func(env string, expected string) {
334+
envMap, _ := Unmarshal(env)
335+
actual, _ := Marshal(envMap)
336+
if expected != actual {
337+
t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual)
338+
}
339+
}
340+
//just test some single lines to show the general idea
341+
//TestRoundtrip makes most of the good assertions
342+
343+
//values are always double-quoted
344+
writeAndCompare(`key=value`, `key="value"`)
345+
//double-quotes are escaped
346+
writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
347+
//but single quotes are left alone
348+
writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
349+
// newlines, backslashes, and some other special chars are escaped
350+
writeAndCompare(`foo="$ba\n\r\\r!"`, `foo="\$ba\n\r\\r\!"`)
351+
}
352+
353+
func TestRoundtrip(t *testing.T) {
354+
fixtures := []string{"equals.env", "exported.env", "invalid1.env", "plain.env", "quoted.env"}
355+
for _, fixture := range fixtures {
356+
fixtureFilename := fmt.Sprintf("fixtures/%s", fixture)
357+
env, err := readFile(fixtureFilename)
358+
if err != nil {
359+
continue
360+
}
361+
rep, err := Marshal(env)
362+
if err != nil {
363+
continue
364+
}
365+
roundtripped, err := Unmarshal(rep)
366+
if err != nil {
367+
continue
368+
}
369+
if !reflect.DeepEqual(env, roundtripped) {
370+
t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
371+
}
372+
373+
}
374+
}

0 commit comments

Comments
 (0)