Skip to content

Commit

Permalink
Add a naïve parser for .env files (#89)
Browse files Browse the repository at this point in the history
* Add a naïve parser for .env files

* Remove dead code

* Cleanup
  • Loading branch information
peterbourgon committed Jan 1, 2022
1 parent f564689 commit 72c982b
Show file tree
Hide file tree
Showing 17 changed files with 311 additions and 96 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -104,7 +104,7 @@ func main() {
port = fs.Int("port", 8080, "listen port for server (also via PORT)")
debug = fs.Bool("debug", false, "log debug information (also via DEBUG)")
)
ff.Parse(fs, os.Args[1:], ff.WithEnvVarNoPrefix())
ff.Parse(fs, os.Args[1:], ff.WithEnvVars())

fmt.Printf("port %d, debug %v\n", *port, *debug)
}
Expand Down
58 changes: 58 additions & 0 deletions env_parser.go
@@ -0,0 +1,58 @@
package ff

import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
)

// EnvParser is a parser for .env files. Each line is tokenized on the first `=`
// character. The first token is interpreted as the flag name, and the second
// token is interpreted as the value. Both tokens are trimmed of leading and
// trailing whitespace. If the value is "double quoted", control characters like
// `\n` are expanded. Lines beginning with `#` are interpreted as comments.
//
// EnvParser respects WithEnvVarPrefix, e.g. an .env file containing `A_B=c`
// will set a flag named "b" if Parse is called with WithEnvVarPrefix("A").
func EnvParser(r io.Reader, set func(name, value string) error) error {
s := bufio.NewScanner(r)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" {
continue // skip empties
}

if line[0] == '#' {
continue // skip comments
}

index := strings.IndexRune(line, '=')
if index < 0 {
return fmt.Errorf("invalid line: %s", line)
}

var (
name = strings.TrimSpace(line[:index])
value = strings.TrimSpace(line[index+1:])
)

if len(name) <= 0 {
return fmt.Errorf("invalid line: %s", line)
}

if len(value) <= 0 {
return fmt.Errorf("invalid line: %s", line)
}

if unquoted, err := strconv.Unquote(value); err == nil {
value = unquoted
}

if err := set(name, value); err != nil {
return err
}
}
return nil
}
66 changes: 66 additions & 0 deletions env_parser_test.go
@@ -0,0 +1,66 @@
package ff_test

import (
"path/filepath"
"testing"
"time"

"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/fftest"
)

func TestEnvFileParser(t *testing.T) {
t.Parallel()

for _, testcase := range []struct {
file string
opts []ff.Option
want fftest.Vars
}{
{
file: "testdata/empty.env",
want: fftest.Vars{},
},
{
file: "testdata/basic.env",
want: fftest.Vars{S: "bar", I: 99, B: true, D: time.Hour},
},
{
file: "testdata/prefix.env",
opts: []ff.Option{ff.WithEnvVarPrefix("MYPROG")},
want: fftest.Vars{S: "bingo", I: 123},
},
{
file: "testdata/prefix-undef.env",
opts: []ff.Option{ff.WithEnvVarPrefix("MYPROG"), ff.WithIgnoreUndefined(true)},
want: fftest.Vars{S: "bango", I: 9},
},
{
file: "testdata/quotes.env",
want: fftest.Vars{S: "", I: 32, X: []string{"1", "2 2", "3 3 3"}},
},
{
file: "testdata/no-value.env",
want: fftest.Vars{WantParseErrorString: "invalid line: D="},
},
{
file: "testdata/spaces.env",
want: fftest.Vars{X: []string{"1", "2", "3", "4", "5", " 6", " 7 ", " 8 "}},
},
{
file: "testdata/newlines.env",
want: fftest.Vars{S: "one\ntwo\nthree\n\n", X: []string{`A\nB\n\n`}},
},
{
file: "testdata/capitalization.env",
want: fftest.Vars{S: "hello", I: 12345},
},
} {
t.Run(filepath.Base(testcase.file), func(t *testing.T) {
testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ff.EnvParser))
fs, vars := fftest.Pair()
vars.ParseError = ff.Parse(fs, []string{}, testcase.opts...)
fftest.Compare(t, &testcase.want, vars)
})
}
}
4 changes: 2 additions & 2 deletions ffcli/command_test.go
Expand Up @@ -355,7 +355,7 @@ func TestIssue57(t *testing.T) {
},
{
args: []string{"bar", "-undefined"},
parseErrStr: "error parsing commandline args: flag provided but not defined: -undefined",
parseErrStr: "error parsing commandline arguments: flag provided but not defined: -undefined",
runErrIs: ffcli.ErrUnparsed,
},
{
Expand All @@ -368,7 +368,7 @@ func TestIssue57(t *testing.T) {
},
{
args: []string{"bar", "baz", "-also.undefined"},
parseErrStr: "error parsing commandline args: flag provided but not defined: -also.undefined",
parseErrStr: "error parsing commandline arguments: flag provided but not defined: -also.undefined",
runErrIs: ffcli.ErrUnparsed,
},
} {
Expand Down
1 change: 1 addition & 0 deletions options.go
@@ -0,0 +1 @@
package ff

0 comments on commit 72c982b

Please sign in to comment.