Skip to content

Commit 49efa50

Browse files
committed
introduce ability to use custom env_file format
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 8158217 commit 49efa50

File tree

7 files changed

+109
-17
lines changed

7 files changed

+109
-17
lines changed

dotenv/fixtures/custom.format

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FOO:BAR
2+
ZOT:QIX

dotenv/format.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2020 The Compose Specification Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dotenv
18+
19+
import (
20+
"fmt"
21+
"io"
22+
)
23+
24+
var formats = map[string]Parser{}
25+
26+
type Parser func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error)
27+
28+
func RegisterFormat(format string, p Parser) {
29+
formats[format] = p
30+
}
31+
32+
func ParseWithFormat(r io.Reader, filename string, resolve LookupFn, format string) (map[string]string, error) {
33+
parser, ok := formats[format]
34+
if !ok {
35+
return nil, fmt.Errorf("unsupported env_file format %q", format)
36+
}
37+
return parser(r, filename, resolve)
38+
}

dotenv/godotenv.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string,
8686
envMap := make(map[string]string)
8787

8888
for _, filename := range filenames {
89-
individualEnvMap, individualErr := readFile(filename, lookupFn)
89+
individualEnvMap, individualErr := ReadFile(filename, lookupFn)
9090

9191
if individualErr != nil {
9292
return envMap, individualErr
@@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string {
129129
}
130130

131131
func loadFile(filename string, overload bool) error {
132-
envMap, err := readFile(filename, nil)
132+
envMap, err := ReadFile(filename, nil)
133133
if err != nil {
134134
return err
135135
}
@@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error {
150150
return nil
151151
}
152152

153-
func readFile(filename string, lookupFn LookupFn) (map[string]string, error) {
153+
func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) {
154154
file, err := os.Open(filename)
155155
if err != nil {
156156
return nil, err

dotenv/godotenv_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dotenv
22

33
import (
4+
"bufio"
45
"bytes"
56
"errors"
7+
"io"
68
"os"
79
"path/filepath"
810
"strings"
@@ -708,3 +710,35 @@ func TestGetEnvFromFile(t *testing.T) {
708710
_, err = GetEnvFromFile(nil, []string{f})
709711
assert.Check(t, strings.HasSuffix(err.Error(), ".env is a directory"))
710712
}
713+
714+
func TestLoadWithFormat(t *testing.T) {
715+
envFileName := "fixtures/custom.format"
716+
expectedValues := map[string]string{
717+
"FOO": "BAR",
718+
"ZOT": "QIX",
719+
}
720+
721+
custom := func(r io.Reader, f string, lookup func(key string) (string, bool)) (map[string]string, error) {
722+
vars := map[string]string{}
723+
scanner := bufio.NewScanner(r)
724+
for scanner.Scan() {
725+
key, value, found := strings.Cut(scanner.Text(), ":")
726+
if !found {
727+
value, found = lookup(key)
728+
if !found {
729+
continue
730+
}
731+
}
732+
vars[key] = value
733+
}
734+
return vars, nil
735+
}
736+
737+
RegisterFormat("custom", custom)
738+
739+
f, err := os.Open(envFileName)
740+
assert.NilError(t, err)
741+
env, err := ParseWithFormat(f, envFileName, nil, "custom")
742+
assert.NilError(t, err)
743+
assert.DeepEqual(t, expectedValues, env)
744+
}

schema/compose-spec.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,9 @@
831831
"path": {
832832
"type": "string"
833833
},
834+
"format": {
835+
"type": "string"
836+
},
834837
"required": {
835838
"type": ["boolean", "string"],
836839
"default": true

types/envfile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
type EnvFile struct {
2424
Path string `yaml:"path,omitempty" json:"path,omitempty"`
2525
Required bool `yaml:"required" json:"required"`
26+
Format string `yaml:"format,omitempty" json:"format,omitempty"`
2627
}
2728

2829
// MarshalYAML makes EnvFile implement yaml.Marshaler

types/project.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -616,22 +616,11 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
616616
}
617617

618618
for _, envFile := range service.EnvFiles {
619-
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
620-
if envFile.Required {
621-
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
622-
}
623-
continue
624-
}
625-
b, err := os.ReadFile(envFile.Path)
619+
vars, err := loadEnvFile(envFile, resolve)
626620
if err != nil {
627-
return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err)
621+
return nil, err
628622
}
629-
630-
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
631-
if err != nil {
632-
return nil, fmt.Errorf("failed to read %s: %w", envFile.Path, err)
633-
}
634-
environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
623+
environment.OverrideBy(vars.ToMappingWithEquals())
635624
}
636625

637626
service.Environment = environment.OverrideBy(service.Environment)
@@ -644,6 +633,31 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
644633
return newProject, nil
645634
}
646635

636+
func loadEnvFile(envFile EnvFile, resolve dotenv.LookupFn) (Mapping, error) {
637+
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
638+
if envFile.Required {
639+
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
640+
}
641+
return nil, nil
642+
}
643+
file, err := os.Open(envFile.Path)
644+
if err != nil {
645+
return nil, err
646+
}
647+
defer file.Close() //nolint:errcheck
648+
649+
var fileVars map[string]string
650+
if envFile.Format != "" {
651+
fileVars, err = dotenv.ParseWithFormat(file, envFile.Path, resolve, envFile.Format)
652+
} else {
653+
fileVars, err = dotenv.ParseWithLookup(file, resolve)
654+
}
655+
if err != nil {
656+
return nil, err
657+
}
658+
return fileVars, nil
659+
}
660+
647661
func (p *Project) deepCopy() *Project {
648662
if p == nil {
649663
return nil

0 commit comments

Comments
 (0)