Skip to content

Commit 840dbdb

Browse files
authored
Merge pull request #342 from ndeloof/env_error
2 parents 2e9f19c + 784a7e7 commit 840dbdb

File tree

6 files changed

+45
-158
lines changed

6 files changed

+45
-158
lines changed

cli/options.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ func GetEnvFromFile(currentEnv map[string]string, workingDir string, filename st
238238

239239
file, err := os.Open(dotEnvFile)
240240
if err != nil {
241-
return envMap, err
241+
return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile)
242242
}
243243
defer file.Close()
244244

@@ -250,7 +250,7 @@ func GetEnvFromFile(currentEnv map[string]string, workingDir string, filename st
250250
return v, true
251251
})
252252
if err != nil {
253-
return envMap, err
253+
return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile)
254254
}
255255
for k, v := range env {
256256
envMap[k] = v

dotenv/fixtures/invalid1.env

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1+
# some comments
2+
foo="
3+
a
4+
multine
5+
value
6+
"
17
INVALID LINE
2-
foo=bar
8+
zot=qix

dotenv/godotenv.go

+1-105
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,9 @@ package dotenv
1515

1616
import (
1717
"bytes"
18-
"fmt"
1918
"io"
2019
"os"
21-
"os/exec"
2220
"regexp"
23-
"sort"
24-
"strconv"
2521
"strings"
2622

2723
"github.com/compose-spec/compose-go/template"
@@ -72,21 +68,6 @@ func Load(filenames ...string) error {
7268
return load(false, filenames...)
7369
}
7470

75-
// Overload will read your env file(s) and load them into ENV for this process.
76-
//
77-
// Call this function as close as possible to the start of your program (ideally in main).
78-
//
79-
// If you call Overload without any args it will default to loading .env in the current path.
80-
//
81-
// You can otherwise tell it which files to load (there can be more than one) like:
82-
//
83-
// godotenv.Overload("fileone", "filetwo")
84-
//
85-
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
86-
func Overload(filenames ...string) error {
87-
return load(true, filenames...)
88-
}
89-
9071
func load(overload bool, filenames ...string) error {
9172
filenames = filenamesOrDefault(filenames)
9273
for _, filename := range filenames {
@@ -128,82 +109,13 @@ func Read(filenames ...string) (map[string]string, error) {
128109
return ReadWithLookup(nil, filenames...)
129110
}
130111

131-
// Unmarshal reads an env file from a string, returning a map of keys and values.
132-
func Unmarshal(str string) (map[string]string, error) {
133-
return UnmarshalBytes([]byte(str))
134-
}
135-
136-
// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
137-
func UnmarshalBytes(src []byte) (map[string]string, error) {
138-
return UnmarshalBytesWithLookup(src, nil)
139-
}
140-
141112
// UnmarshalBytesWithLookup parses env file from byte slice of chars, returning a map of keys and values.
142113
func UnmarshalBytesWithLookup(src []byte, lookupFn LookupFn) (map[string]string, error) {
143114
out := make(map[string]string)
144-
err := parseBytes(src, out, lookupFn)
115+
err := newParser().parseBytes(src, out, lookupFn)
145116
return out, err
146117
}
147118

148-
// Exec loads env vars from the specified filenames (empty map falls back to default)
149-
// then executes the cmd specified.
150-
//
151-
// Simply hooks up os.Stdin/err/out to the command and calls Run()
152-
//
153-
// If you want more fine grained control over your command it's recommended
154-
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
155-
//
156-
// Deprecated: Use the `os/exec` package directly.
157-
func Exec(filenames []string, cmd string, cmdArgs []string) error {
158-
if err := Load(filenames...); err != nil {
159-
return err
160-
}
161-
162-
command := exec.Command(cmd, cmdArgs...)
163-
command.Stdin = os.Stdin
164-
command.Stdout = os.Stdout
165-
command.Stderr = os.Stderr
166-
return command.Run()
167-
}
168-
169-
// Write serializes the given environment and writes it to a file
170-
//
171-
// Deprecated: The serialization functions are untested and unmaintained.
172-
func Write(envMap map[string]string, filename string) error {
173-
//goland:noinspection GoDeprecation
174-
content, err := Marshal(envMap)
175-
if err != nil {
176-
return err
177-
}
178-
file, err := os.Create(filename)
179-
if err != nil {
180-
return err
181-
}
182-
defer file.Close()
183-
_, err = file.WriteString(content + "\n")
184-
if err != nil {
185-
return err
186-
}
187-
return file.Sync()
188-
}
189-
190-
// Marshal outputs the given environment as a dotenv-formatted environment file.
191-
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
192-
//
193-
// Deprecated: The serialization functions are untested and unmaintained.
194-
func Marshal(envMap map[string]string) (string, error) {
195-
lines := make([]string, 0, len(envMap))
196-
for k, v := range envMap {
197-
if d, err := strconv.Atoi(v); err == nil {
198-
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
199-
} else {
200-
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) // nolint // Cannot use %q here
201-
}
202-
}
203-
sort.Strings(lines)
204-
return strings.Join(lines, "\n"), nil
205-
}
206-
207119
func filenamesOrDefault(filenames []string) []string {
208120
if len(filenames) == 0 {
209121
return []string{".env"}
@@ -255,19 +167,3 @@ func expandVariables(value string, envMap map[string]string, lookupFn LookupFn)
255167
}
256168
return retVal, nil
257169
}
258-
259-
// Deprecated: only used by unsupported/untested code for Marshal/Write.
260-
func doubleQuoteEscape(line string) string {
261-
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
262-
for _, c := range doubleQuoteSpecialChars {
263-
toReplace := "\\" + string(c)
264-
if c == '\n' {
265-
toReplace = `\n`
266-
}
267-
if c == '\r' {
268-
toReplace = `\r`
269-
}
270-
line = strings.ReplaceAll(line, string(c), toReplace)
271-
}
272-
return line
273-
}

dotenv/godotenv_test.go

+3-34
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,13 @@ func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
5555
}
5656
}
5757

58-
func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
59-
err := Overload()
60-
pathError := err.(*os.PathError)
61-
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
62-
t.Errorf("Didn't try and open .env by default")
63-
}
64-
}
65-
6658
func TestLoadFileNotFound(t *testing.T) {
6759
err := Load("somefilethatwillneverexistever.env")
6860
if err == nil {
6961
t.Error("File wasn't found but Load didn't return an error")
7062
}
7163
}
7264

73-
func TestOverloadFileNotFound(t *testing.T) {
74-
err := Overload("somefilethatwillneverexistever.env")
75-
if err == nil {
76-
t.Error("File wasn't found but Overload didn't return an error")
77-
}
78-
}
79-
8065
func TestReadPlainEnv(t *testing.T) {
8166
envFileName := "fixtures/plain.env"
8267
expectedValues := map[string]string{
@@ -139,20 +124,6 @@ func TestLoadDoesNotOverride(t *testing.T) {
139124
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
140125
}
141126

142-
func TestOverloadDoesOverride(t *testing.T) {
143-
envFileName := "fixtures/plain.env"
144-
145-
// ensure NO overload
146-
presets := map[string]string{
147-
"OPTION_A": "do_not_override",
148-
}
149-
150-
expectedValues := map[string]string{
151-
"OPTION_A": "1",
152-
}
153-
loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets)
154-
}
155-
156127
func TestLoadPlainEnv(t *testing.T) {
157128
envFileName := "fixtures/plain.env"
158129
expectedValues := map[string]string{
@@ -494,7 +465,7 @@ func TestLinesToIgnore(t *testing.T) {
494465

495466
for n, c := range cases {
496467
t.Run(n, func(t *testing.T) {
497-
got := string(getStatementStart([]byte(c.input)))
468+
got := string(newParser().getStatementStart([]byte(c.input)))
498469
if got != c.want {
499470
t.Errorf("Expected:\t %q\nGot:\t %q", c.want, got)
500471
}
@@ -513,10 +484,8 @@ func TestErrorReadDirectory(t *testing.T) {
513484

514485
func TestErrorParsing(t *testing.T) {
515486
envFileName := "fixtures/invalid1.env"
516-
envMap, err := Read(envFileName)
517-
if err == nil {
518-
t.Errorf("Expected error, got %v", envMap)
519-
}
487+
_, err := Read(envFileName)
488+
assert.ErrorContains(t, err, "line 7: key cannot contain a space")
520489
}
521490

522491
func TestInheritedEnvVariableSameSize(t *testing.T) {

dotenv/parser.go

+31-15
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,34 @@ var (
2121
exportRegex = regexp.MustCompile(`^export\s+`)
2222
)
2323

24-
func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
24+
type parser struct {
25+
line int
26+
}
27+
28+
func newParser() *parser {
29+
return &parser{
30+
line: 1,
31+
}
32+
}
33+
34+
func (p *parser) parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
2535
cutset := src
2636
if lookupFn == nil {
2737
lookupFn = noLookupFn
2838
}
2939
for {
30-
cutset = getStatementStart(cutset)
40+
cutset = p.getStatementStart(cutset)
3141
if cutset == nil {
3242
// reached end of file
3343
break
3444
}
3545

36-
key, left, inherited, err := locateKeyName(cutset)
46+
key, left, inherited, err := p.locateKeyName(cutset)
3747
if err != nil {
3848
return err
3949
}
4050
if strings.Contains(key, " ") {
41-
return errors.New("key cannot contain a space")
51+
return fmt.Errorf("line %d: key cannot contain a space", p.line)
4252
}
4353

4454
if inherited {
@@ -50,7 +60,7 @@ func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
5060
continue
5161
}
5262

53-
value, left, err := extractVarValue(left, out, lookupFn)
63+
value, left, err := p.extractVarValue(left, out, lookupFn)
5464
if err != nil {
5565
return err
5666
}
@@ -65,8 +75,8 @@ func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
6575
// getStatementPosition returns position of statement begin.
6676
//
6777
// It skips any comment line or non-whitespace character.
68-
func getStatementStart(src []byte) []byte {
69-
pos := indexOfNonSpaceChar(src)
78+
func (p *parser) getStatementStart(src []byte) []byte {
79+
pos := p.indexOfNonSpaceChar(src)
7080
if pos == -1 {
7181
return nil
7282
}
@@ -81,12 +91,11 @@ func getStatementStart(src []byte) []byte {
8191
if pos == -1 {
8292
return nil
8393
}
84-
85-
return getStatementStart(src[pos:])
94+
return p.getStatementStart(src[pos:])
8695
}
8796

8897
// locateKeyName locates and parses key name and returns rest of slice
89-
func locateKeyName(src []byte) (string, []byte, bool, error) {
98+
func (p *parser) locateKeyName(src []byte) (string, []byte, bool, error) {
9099
var key string
91100
var inherited bool
92101
// trim "export" and space at beginning
@@ -116,8 +125,8 @@ loop:
116125
}
117126

118127
return "", nil, inherited, fmt.Errorf(
119-
`unexpected character %q in variable name near %q`,
120-
string(char), string(src))
128+
`line %d: unexpected character %q in variable name`,
129+
p.line, string(char))
121130
}
122131
}
123132

@@ -132,11 +141,12 @@ loop:
132141
}
133142

134143
// extractVarValue extracts variable value and returns rest of slice
135-
func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (string, []byte, error) {
144+
func (p *parser) extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (string, []byte, error) {
136145
quote, isQuoted := hasQuotePrefix(src)
137146
if !isQuoted {
138147
// unquoted value - read until new line
139148
value, rest, _ := bytes.Cut(src, []byte("\n"))
149+
p.line++
140150

141151
// Remove inline comments on unquoted lines
142152
value, _, _ = bytes.Cut(value, []byte(" #"))
@@ -147,6 +157,9 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (s
147157

148158
// lookup quoted string terminator
149159
for i := 1; i < len(src); i++ {
160+
if src[i] == '\n' {
161+
p.line++
162+
}
150163
if char := src[i]; char != quote {
151164
continue
152165
}
@@ -177,7 +190,7 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (s
177190
valEndIndex = len(src)
178191
}
179192

180-
return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
193+
return "", nil, fmt.Errorf("line %d: unterminated quoted value %s", p.line, src[:valEndIndex])
181194
}
182195

183196
func expandEscapes(str string) string {
@@ -212,8 +225,11 @@ func expandEscapes(str string) string {
212225
return out
213226
}
214227

215-
func indexOfNonSpaceChar(src []byte) int {
228+
func (p *parser) indexOfNonSpaceChar(src []byte) int {
216229
return bytes.IndexFunc(src, func(r rune) bool {
230+
if r == '\n' {
231+
p.line++
232+
}
217233
return !unicode.IsSpace(r)
218234
})
219235
}

loader/loader.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
657657

658658
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
659659
if err != nil {
660-
return err
660+
return errors.Wrapf(err, "Failed to load %s", filePath)
661661
}
662662
env := types.MappingWithEquals{}
663663
for k, v := range fileVars {

0 commit comments

Comments
 (0)