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

Commit 934be71

Browse files
authored
Merge pull request #2 from x1unix/feat/value-separator
refactor dotenv parser in order to support multi-line variable values declaration
2 parents 6fd60f3 + 993ff7a commit 934be71

File tree

3 files changed

+288
-64
lines changed

3 files changed

+288
-64
lines changed

godotenv.go

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
package godotenv
1515

1616
import (
17-
"bufio"
1817
"errors"
1918
"fmt"
2019
"io"
20+
"io/ioutil"
2121
"os"
2222
"os/exec"
2323
"regexp"
@@ -28,6 +28,16 @@ import (
2828

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

31+
// Parse reads an env file from io.Reader, returning a map of keys and values.
32+
func Parse(r io.Reader) (map[string]string, error) {
33+
data, err := ioutil.ReadAll(r)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
return UnmarshalBytes(data)
39+
}
40+
3141
// Load will read your env file(s) and load them into ENV for this process.
3242
//
3343
// Call this function as close as possible to the start of your program (ideally in main)
@@ -96,37 +106,16 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
96106
return
97107
}
98108

99-
// Parse reads an env file from io.Reader, returning a map of keys and values.
100-
func Parse(r io.Reader) (envMap map[string]string, err error) {
101-
envMap = make(map[string]string)
102-
103-
var lines []string
104-
scanner := bufio.NewScanner(r)
105-
for scanner.Scan() {
106-
lines = append(lines, scanner.Text())
107-
}
108-
109-
if err = scanner.Err(); err != nil {
110-
return
111-
}
112-
113-
for _, fullLine := range lines {
114-
if !isIgnoredLine(fullLine) {
115-
var key, value string
116-
key, value, err = parseLine(fullLine, envMap)
117-
118-
if err != nil {
119-
return
120-
}
121-
envMap[key] = value
122-
}
123-
}
124-
return
109+
// Unmarshal reads an env file from a string, returning a map of keys and values.
110+
func Unmarshal(str string) (envMap map[string]string, err error) {
111+
return UnmarshalBytes([]byte(str))
125112
}
126113

127-
//Unmarshal reads an env file from a string, returning a map of keys and values.
128-
func Unmarshal(str string) (envMap map[string]string, err error) {
129-
return Parse(strings.NewReader(str))
114+
// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
115+
func UnmarshalBytes(src []byte) (map[string]string, error) {
116+
out := make(map[string]string)
117+
err := parseBytes(src, out)
118+
return out, err
130119
}
131120

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

142133
command := exec.Command(cmd, cmdArgs...)
143134
command.Stdin = os.Stdin
@@ -161,8 +152,7 @@ func Write(envMap map[string]string, filename string) error {
161152
if err != nil {
162153
return err
163154
}
164-
file.Sync()
165-
return err
155+
return file.Sync()
166156
}
167157

168158
// Marshal outputs the given environment as a dotenv-formatted environment file.
@@ -202,7 +192,7 @@ func loadFile(filename string, overload bool) error {
202192

203193
for key, value := range envMap {
204194
if !currentEnv[key] || overload {
205-
os.Setenv(key, value)
195+
_ = os.Setenv(key, value)
206196
}
207197
}
208198

@@ -343,11 +333,6 @@ func expandVariables(v string, m map[string]string) string {
343333
})
344334
}
345335

346-
func isIgnoredLine(line string) bool {
347-
trimmedLine := strings.TrimSpace(line)
348-
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
349-
}
350-
351336
func doubleQuoteEscape(line string) string {
352337
for _, c := range doubleQuoteSpecialChars {
353338
toReplace := "\\" + string(c)

godotenv_test.go

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,34 @@ func TestExpanding(t *testing.T) {
271271

272272
}
273273

274+
func TestVariableStringValueSeparator(t *testing.T) {
275+
input := "TEST_URLS=\"stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443\""
276+
want := map[string]string{
277+
"TEST_URLS": "stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443",
278+
}
279+
got, err := Parse(strings.NewReader(input))
280+
if err != nil {
281+
t.Error(err)
282+
}
283+
284+
if len(got) != len(want) {
285+
t.Fatalf(
286+
"unexpected value:\nwant:\n\t%#v\n\ngot:\n\t%#v", want, got)
287+
}
288+
289+
for k, wantVal := range want {
290+
gotVal, ok := got[k]
291+
if !ok {
292+
t.Fatalf("key %q doesn't present in result", k)
293+
}
294+
if wantVal != gotVal {
295+
t.Fatalf(
296+
"mismatch in %q value:\nwant:\n\t%s\n\ngot:\n\t%s", k,
297+
wantVal, gotVal)
298+
}
299+
}
300+
}
301+
274302
func TestActualEnvVarsAreLeftAlone(t *testing.T) {
275303
os.Clearenv()
276304
os.Setenv("OPTION_A", "actualenv")
@@ -377,33 +405,38 @@ func TestParsing(t *testing.T) {
377405
}
378406

379407
func TestLinesToIgnore(t *testing.T) {
380-
// it 'ignores empty lines' do
381-
// expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz')
382-
if !isIgnoredLine("\n") {
383-
t.Error("Line with nothing but line break wasn't ignored")
384-
}
385-
386-
if !isIgnoredLine("\r\n") {
387-
t.Error("Line with nothing but windows-style line break wasn't ignored")
388-
}
389-
390-
if !isIgnoredLine("\t\t ") {
391-
t.Error("Line full of whitespace wasn't ignored")
392-
}
393-
394-
// it 'ignores comment lines' do
395-
// expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar')
396-
if !isIgnoredLine("# comment") {
397-
t.Error("Comment wasn't ignored")
398-
}
399-
400-
if !isIgnoredLine("\t#comment") {
401-
t.Error("Indented comment wasn't ignored")
408+
cases := map[string]struct {
409+
input string
410+
want string
411+
}{
412+
"Line with nothing but line break": {
413+
input: "\n",
414+
},
415+
"Line with nothing but windows-style line break": {
416+
input: "\r\n",
417+
},
418+
"Line full of whitespace": {
419+
input: "\t\t ",
420+
},
421+
"Comment": {
422+
input: "# Comment",
423+
},
424+
"Indented comment": {
425+
input: "\t # comment",
426+
},
427+
"non-ignored value": {
428+
input: `export OPTION_B='\n'`,
429+
want: `export OPTION_B='\n'`,
430+
},
402431
}
403432

404-
// make sure we're not getting false positives
405-
if isIgnoredLine(`export OPTION_B='\n'`) {
406-
t.Error("ignoring a perfectly valid line to parse")
433+
for n, c := range cases {
434+
t.Run(n, func(t *testing.T) {
435+
got := string(getStatementStart([]byte(c.input)))
436+
if got != c.want {
437+
t.Errorf("Expected:\t %q\nGot:\t %q", c.want, got)
438+
}
439+
})
407440
}
408441
}
409442

0 commit comments

Comments
 (0)