forked from coop-cloud-mirrors/godotenv
Merge pull request #35 from alexquick/feature-write-dotenv
support for writing envs out in dotenv format
This commit is contained in:
46
godotenv.go
46
godotenv.go
@ -16,6 +16,7 @@ package godotenv
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -23,6 +24,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||||
|
|
||||||
// Load will read your env file(s) and load them into ENV for this process.
|
// 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)
|
// 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) {
|
|||||||
return
|
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)
|
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||||
// then executes the cmd specified.
|
// then executes the cmd specified.
|
||||||
//
|
//
|
||||||
@ -136,6 +144,30 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error {
|
|||||||
return command.Run()
|
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 {
|
func filenamesOrDefault(filenames []string) []string {
|
||||||
if len(filenames) == 0 {
|
if len(filenames) == 0 {
|
||||||
return []string{".env"}
|
return []string{".env"}
|
||||||
@ -264,3 +296,17 @@ func isIgnoredLine(line string) bool {
|
|||||||
trimmedLine := strings.Trim(line, " \n\t")
|
trimmedLine := strings.Trim(line, " \n\t")
|
||||||
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
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
|
||||||
|
}
|
||||||
|
@ -2,7 +2,9 @@ package godotenv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -326,3 +328,47 @@ func TestErrorParsing(t *testing.T) {
|
|||||||
t.Errorf("Expected error, got %v", envMap)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user