diff --git a/godotenv.go b/godotenv.go index 466f2eb..7712f94 100644 --- a/godotenv.go +++ b/godotenv.go @@ -26,6 +26,23 @@ import ( "strings" ) +type ParseError struct { + Line uint + Err error +} + +func (e *ParseError) Error() string { + if e.Line == 0 { + return fmt.Sprintf("parse error before reading: %s", e.Err.Error()) + } + return fmt.Sprintf("parse error on line %d: %s", e.Line, e.Err.Error()) +} + +func IsParseError(err error) bool { + _, ok := err.(*ParseError) + return ok +} + const doubleQuoteSpecialChars = "\\\n\r\"!$`" // Load will read your env file(s) and load them into ENV for this process. @@ -99,6 +116,7 @@ func Read(filenames ...string) (envMap map[string]string, err error) { // 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) + err = nil var lines []string scanner := bufio.NewScanner(r) @@ -106,16 +124,24 @@ func Parse(r io.Reader) (envMap map[string]string, err error) { lines = append(lines, scanner.Text()) } - if err = scanner.Err(); err != nil { + if e := scanner.Err(); e != nil { + err = &ParseError{ + Err: e, + } return } - for _, fullLine := range lines { + for line, fullLine := range lines { if !isIgnoredLine(fullLine) { var key, value string - key, value, err = parseLine(fullLine, envMap) + var e error + key, value, e = parseLine(fullLine, envMap) - if err != nil { + if e != nil { + err = &ParseError{ + Line: uint(line) + 1, + Err: e, + } return } envMap[key] = value diff --git a/godotenv_test.go b/godotenv_test.go index 7274c14..1b030e9 100644 --- a/godotenv_test.go +++ b/godotenv_test.go @@ -2,6 +2,7 @@ package godotenv import ( "bytes" + "errors" "fmt" "os" "reflect" @@ -115,6 +116,68 @@ func TestParse(t *testing.T) { } } +func TestIsParseError(t *testing.T) { + err := errors.New("test error") + if IsParseError(err) { + t.Errorf("Expected IsParseError(\"%s\") to be false, got true", err) + } + + err = &ParseError{ + Line: 1, + Err: errors.New("test error inside"), + } + if !IsParseError(err) { + t.Errorf("Expected IsParseError(\"%s\") to be true, got false", err) + } +} + +func TestErrorParse(t *testing.T) { + envMap, err := Parse(bytes.NewReader([]byte("ONE=1\nTWO\nTHREE = \"3\""))) + if err == nil { + t.Errorf("Expected error, got %v", envMap) + } + + pErr, ok := err.(*ParseError) + + if !ok { + t.Fatalf("expected to be ParseError, got %d", err) + } + + if pErr.Line != 2 { + t.Fatalf("expected parse error line to be %d, got \"%s\"", 2, pErr) + } + + if !strings.Contains(err.Error(), pErr.Err.Error()) { + t.Fatalf("expected parse error contain underlying error \"%s\", got \"%s\"", pErr.Err, err) + } +} + +type testBrokenReader struct{} + +func (testBrokenReader) Read([]byte) (int, error) { + return 0, errors.New("reader error") +} +func TestErrorParseNoLine(t *testing.T) { + envMap, err := Parse(testBrokenReader{}) + if err == nil { + t.Errorf("Expected error, got %v", envMap) + } + + pErr, ok := err.(*ParseError) + + if !ok { + t.Fatalf("expected to be ParseError, got %d", err) + } + + if pErr.Line != 0 { + t.Fatalf("expected parse error line to be %d, got \"%s\"", 0, pErr) + } + + if strings.Contains(strings.ToLower(err.Error()), "line") { + t.Fatalf("expected parse error not contain \"line\", got \"%s\"", pErr) + } +} + func TestLoadDoesNotOverride(t *testing.T) { envFileName := "fixtures/plain.env"