Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a naïve parser for .env files #89

Merged
merged 3 commits into from Jan 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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