Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-line values support #118

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 24 additions & 39 deletions godotenv.go
Expand Up @@ -14,10 +14,10 @@
package godotenv

import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"regexp"
Expand All @@ -27,6 +27,16 @@ import (

const doubleQuoteSpecialChars = "\\\n\r\"!$`"

// Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (map[string]string, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}

return UnmarshalBytes(data)
}

// 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 @@ -95,37 +105,16 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
return
}

// Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (envMap map[string]string, err error) {
envMap = make(map[string]string)

var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}

if err = scanner.Err(); err != nil {
return
}

for _, fullLine := range lines {
if !isIgnoredLine(fullLine) {
var key, value string
key, value, err = parseLine(fullLine, envMap)

if err != nil {
return
}
envMap[key] = value
}
}
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 UnmarshalBytes([]byte(str))
}

//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))
// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
func UnmarshalBytes(src []byte) (map[string]string, error) {
out := make(map[string]string)
err := parseBytes(src, out)
return out, err
}

// Exec loads env vars from the specified filenames (empty map falls back to default)
Expand All @@ -136,7 +125,9 @@ func Unmarshal(str string) (envMap map[string]string, err error) {
// If you want more fine grained control over your command it's recommended
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
func Exec(filenames []string, cmd string, cmdArgs []string) error {
Load(filenames...)
if err := Load(filenames...); err != nil {
return err
}

command := exec.Command(cmd, cmdArgs...)
command.Stdin = os.Stdin
Expand All @@ -160,8 +151,7 @@ func Write(envMap map[string]string, filename string) error {
if err != nil {
return err
}
file.Sync()
return err
return file.Sync()
}

// Marshal outputs the given environment as a dotenv-formatted environment file.
Expand Down Expand Up @@ -197,7 +187,7 @@ func loadFile(filename string, overload bool) error {

for key, value := range envMap {
if !currentEnv[key] || overload {
os.Setenv(key, value)
_ = os.Setenv(key, value)
}
}

Expand Down Expand Up @@ -338,11 +328,6 @@ func expandVariables(v string, m map[string]string) string {
})
}

func isIgnoredLine(line string) bool {
trimmedLine := strings.TrimSpace(line)
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
}

func doubleQuoteEscape(line string) string {
for _, c := range doubleQuoteSpecialChars {
toReplace := "\\" + string(c)
Expand Down
83 changes: 58 additions & 25 deletions godotenv_test.go
Expand Up @@ -271,6 +271,34 @@ func TestExpanding(t *testing.T) {

}

func TestVariableStringValueSeparator(t *testing.T) {
input := "TEST_URLS=\"stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443\""
want := map[string]string{
"TEST_URLS": "stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443",
}
got, err := Parse(strings.NewReader(input))
if err != nil {
t.Error(err)
}

if len(got) != len(want) {
t.Fatalf(
"unexpected value:\nwant:\n\t%#v\n\ngot:\n\t%#v", want, got)
}

for k, wantVal := range want {
gotVal, ok := got[k]
if !ok {
t.Fatalf("key %q doesn't present in result", k)
}
if wantVal != gotVal {
t.Fatalf(
"mismatch in %q value:\nwant:\n\t%s\n\ngot:\n\t%s", k,
wantVal, gotVal)
}
}
}

func TestActualEnvVarsAreLeftAlone(t *testing.T) {
os.Clearenv()
os.Setenv("OPTION_A", "actualenv")
Expand Down Expand Up @@ -377,33 +405,38 @@ func TestParsing(t *testing.T) {
}

func TestLinesToIgnore(t *testing.T) {
// it 'ignores empty lines' do
// expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz')
if !isIgnoredLine("\n") {
t.Error("Line with nothing but line break wasn't ignored")
}

if !isIgnoredLine("\r\n") {
t.Error("Line with nothing but windows-style line break wasn't ignored")
}

if !isIgnoredLine("\t\t ") {
t.Error("Line full of whitespace wasn't ignored")
}

// it 'ignores comment lines' do
// expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar')
if !isIgnoredLine("# comment") {
t.Error("Comment wasn't ignored")
}

if !isIgnoredLine("\t#comment") {
t.Error("Indented comment wasn't ignored")
cases := map[string]struct {
input string
want string
}{
"Line with nothing but line break": {
input: "\n",
},
"Line with nothing but windows-style line break": {
input: "\r\n",
},
"Line full of whitespace": {
input: "\t\t ",
},
"Comment": {
input: "# Comment",
},
"Indented comment": {
input: "\t # comment",
},
"non-ignored value": {
input: `export OPTION_B='\n'`,
want: `export OPTION_B='\n'`,
},
}

// make sure we're not getting false positives
if isIgnoredLine(`export OPTION_B='\n'`) {
t.Error("ignoring a perfectly valid line to parse")
for n, c := range cases {
t.Run(n, func(t *testing.T) {
got := string(getStatementStart([]byte(c.input)))
if got != c.want {
t.Errorf("Expected:\t %q\nGot:\t %q", c.want, got)
}
})
}
}

Expand Down