-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package ffenv | ||
|
||
import ( | ||
"bufio" | ||
"io" | ||
"strings" | ||
) | ||
|
||
// Parser is a parser for .env file format: flag=value. Each | ||
// line is tokenized as a single key/value pair. | ||
func Parser(r io.Reader, set func(name, value string) error) error { | ||
return parse("", r, set) | ||
} | ||
|
||
func parse(prefix string, 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 | ||
} | ||
|
||
line = strings.TrimPrefix(line, prefix+"_") | ||
var ( | ||
name string | ||
value string | ||
index = strings.IndexRune(line, '=') | ||
) | ||
if index < 0 { | ||
name, value = line, "true" // boolean option | ||
} else { | ||
name, value = strings.ToLower(line[:index]), line[index+1:] | ||
name = strings.ReplaceAll(name, "_", "-") | ||
} | ||
|
||
if i := strings.Index(value, " #"); i >= 0 { | ||
value = strings.TrimSpace(value[:i]) | ||
} | ||
|
||
if err := set(name, value); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// ParserWithPrefix removes any prefix_ on keys in a .env file. | ||
// MY_APP_PREFIX_KEY=value will get evaluated as key=value. | ||
func ParserWithPrefix(prefix string) func(io.Reader, func(string, string) error) error { | ||
return func(r io.Reader, set func(name, value string) error) error { | ||
return parse(prefix, r, set) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
package ffenv_test | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
"time" | ||
|
||
"github.com/peterbourgon/ff/v3" | ||
"github.com/peterbourgon/ff/v3/ffenv" | ||
"github.com/peterbourgon/ff/v3/fftest" | ||
) | ||
|
||
func TestParseBasics(t *testing.T) { | ||
t.Parallel() | ||
|
||
for _, testcase := range []struct { | ||
name string | ||
env map[string]string | ||
file string | ||
args []string | ||
opts []ff.Option | ||
want fftest.Vars | ||
}{ | ||
{ | ||
name: "empty", | ||
args: []string{}, | ||
want: fftest.Vars{}, | ||
}, | ||
{ | ||
name: "args only", | ||
args: []string{"-s", "foo", "-i", "123", "-b", "-d", "24m"}, | ||
want: fftest.Vars{S: "foo", I: 123, B: true, D: 24 * time.Minute}, | ||
}, | ||
{ | ||
name: "file only", | ||
file: "testdata/1.env", | ||
want: fftest.Vars{S: "bar", I: 99, B: true, D: time.Hour}, | ||
}, | ||
{ | ||
name: "env only", | ||
env: map[string]string{"TEST_PARSE_S": "baz", "TEST_PARSE_F": "0.99", "TEST_PARSE_D": "100s"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, | ||
want: fftest.Vars{S: "baz", F: 0.99, D: 100 * time.Second}, | ||
}, | ||
{ | ||
name: "file args", | ||
file: "testdata/2.env", | ||
args: []string{"-s", "foo", "-i", "1234"}, | ||
want: fftest.Vars{S: "foo", I: 1234, D: 3 * time.Second}, | ||
}, | ||
{ | ||
name: "env args", | ||
env: map[string]string{"TEST_PARSE_S": "should be overridden", "TEST_PARSE_B": "true"}, | ||
args: []string{"-s", "explicit wins", "-i", "7"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, | ||
want: fftest.Vars{S: "explicit wins", I: 7, B: true}, | ||
}, | ||
{ | ||
name: "file env", | ||
env: map[string]string{"TEST_PARSE_S": "env takes priority", "TEST_PARSE_B": "true"}, | ||
file: "testdata/3.env", | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, | ||
want: fftest.Vars{S: "env takes priority", I: 99, B: true, D: 34 * time.Second}, | ||
}, | ||
{ | ||
name: "file env args", | ||
file: "testdata/4.env", | ||
env: map[string]string{"TEST_PARSE_S": "from env", "TEST_PARSE_I": "300", "TEST_PARSE_F": "0.15", "TEST_PARSE_B": "true"}, | ||
args: []string{"-s", "from arg", "-i", "100"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, | ||
want: fftest.Vars{S: "from arg", I: 100, F: 0.15, B: true, D: time.Minute}, | ||
}, | ||
{ | ||
name: "repeated args", | ||
args: []string{"-s", "foo", "-s", "bar", "-d", "1m", "-d", "1h", "-x", "1", "-x", "2", "-x", "3"}, | ||
want: fftest.Vars{S: "bar", D: time.Hour, X: []string{"1", "2", "3"}}, | ||
}, | ||
{ | ||
name: "priority repeats", | ||
env: map[string]string{"TEST_PARSE_S": "s.env", "TEST_PARSE_X": "x.env.1"}, | ||
file: "testdata/5.env", | ||
args: []string{"-s", "s.arg.1", "-s", "s.arg.2", "-x", "x.arg.1", "-x", "x.arg.2"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, | ||
want: fftest.Vars{S: "s.arg.2", X: []string{"x.arg.1", "x.arg.2"}}, // highest prio wins and no others are called | ||
}, | ||
{ | ||
name: "PlainParser solo bool", | ||
file: "testdata/solo_bool.env", | ||
want: fftest.Vars{S: "x", B: true}, | ||
}, | ||
{ | ||
name: "PlainParser string with spaces", | ||
file: "testdata/equals.env", | ||
want: fftest.Vars{S: "i=am=the=very=model=of=a=modern=major=general"}, | ||
}, | ||
{ | ||
name: "default comma behavior", | ||
env: map[string]string{"TEST_PARSE_S": "one,two,three", "TEST_PARSE_X": "one,two,three"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, | ||
want: fftest.Vars{S: "one,two,three", X: []string{"one,two,three"}}, | ||
}, | ||
{ | ||
name: "WithEnvVarSplit", | ||
env: map[string]string{"TEST_PARSE_S": "one,two,three", "TEST_PARSE_X": "one,two,three"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit(",")}, | ||
want: fftest.Vars{S: "three", X: []string{"one", "two", "three"}}, | ||
}, | ||
{ | ||
name: "WithEnvVarNoPrefix", | ||
env: map[string]string{"TEST_PARSE_S": "foo", "S": "bar"}, | ||
opts: []ff.Option{ff.WithEnvVarNoPrefix()}, | ||
want: fftest.Vars{S: "bar"}, | ||
}, | ||
{ | ||
name: "WithIgnoreUndefined env", | ||
env: map[string]string{"TEST_PARSE_UNDEFINED": "one", "TEST_PARSE_S": "one"}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithIgnoreUndefined(true)}, | ||
want: fftest.Vars{S: "one"}, | ||
}, | ||
{ | ||
name: "WithIgnoreUndefined file true", | ||
file: "testdata/undefined.env", | ||
opts: []ff.Option{ff.WithIgnoreUndefined(true)}, | ||
want: fftest.Vars{S: "one"}, | ||
}, | ||
{ | ||
name: "WithIgnoreUndefined file false", | ||
file: "testdata/undefined.env", | ||
opts: []ff.Option{ff.WithIgnoreUndefined(false)}, | ||
want: fftest.Vars{WantParseErrorString: "config file flag"}, | ||
}, | ||
{ | ||
name: "env var split comma whitespace", | ||
env: map[string]string{"TEST_PARSE_S": "one, two, three ", "TEST_PARSE_X": "one, two, three "}, | ||
opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit(",")}, | ||
want: fftest.Vars{S: " three ", X: []string{"one", " two", " three "}}, | ||
}, | ||
} { | ||
t.Run(testcase.name, func(t *testing.T) { | ||
if testcase.file != "" { | ||
testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.Parser)) | ||
} | ||
|
||
if len(testcase.env) > 0 { | ||
for k, v := range testcase.env { | ||
defer os.Setenv(k, os.Getenv(k)) | ||
os.Setenv(k, v) | ||
} | ||
} | ||
|
||
fs, vars := fftest.Pair() | ||
vars.ParseError = ff.Parse(fs, testcase.args, testcase.opts...) | ||
if err := fftest.Compare(&testcase.want, vars); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestParseIssue16(t *testing.T) { | ||
t.Parallel() | ||
|
||
for _, testcase := range []struct { | ||
name string | ||
data string | ||
want string | ||
}{ | ||
{ | ||
name: "hash in value", | ||
data: "s=bar#baz", | ||
want: "bar#baz", | ||
}, | ||
{ | ||
name: "EOL comment with space", | ||
data: "s=bar # baz", | ||
want: "bar", | ||
}, | ||
{ | ||
name: "EOL comment no space", | ||
data: "s=bar #baz", | ||
want: "bar", | ||
}, | ||
{ | ||
name: "only comment with space", | ||
data: "# foo=bar\n", | ||
want: "", | ||
}, | ||
{ | ||
name: "only comment no space", | ||
data: "#foo=bar\n", | ||
want: "", | ||
}, | ||
} { | ||
t.Run(testcase.name, func(t *testing.T) { | ||
filename, cleanup := fftest.TempFile(t, testcase.data) | ||
defer cleanup() | ||
|
||
fs, vars := fftest.Pair() | ||
vars.ParseError = ff.Parse(fs, []string{}, | ||
ff.WithConfigFile(filename), | ||
ff.WithConfigFileParser(ffenv.Parser), | ||
) | ||
|
||
want := fftest.Vars{S: testcase.want} | ||
if err := fftest.Compare(&want, vars); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestParseConfigFile(t *testing.T) { | ||
t.Parallel() | ||
|
||
for _, testcase := range []struct { | ||
name string | ||
missing bool | ||
allowMissing bool | ||
parseError error | ||
}{ | ||
{ | ||
name: "has config file", | ||
}, | ||
{ | ||
name: "config file missing", | ||
missing: true, | ||
parseError: os.ErrNotExist, | ||
}, | ||
{ | ||
name: "config file missing + allow missing", | ||
missing: true, | ||
allowMissing: true, | ||
}, | ||
} { | ||
t.Run(testcase.name, func(t *testing.T) { | ||
filename := "dummy" | ||
if !testcase.missing { | ||
var cleanup func() | ||
filename, cleanup = fftest.TempFile(t, "") | ||
defer cleanup() | ||
} | ||
|
||
options := []ff.Option{ff.WithConfigFile(filename), ff.WithConfigFileParser(ffenv.Parser)} | ||
if testcase.allowMissing { | ||
options = append(options, ff.WithAllowMissingConfigFile(true)) | ||
} | ||
|
||
fs, vars := fftest.Pair() | ||
vars.ParseError = ff.Parse(fs, []string{}, options...) | ||
|
||
want := fftest.Vars{WantParseErrorIs: testcase.parseError} | ||
if err := fftest.Compare(&want, vars); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestParseClassicEnvFileWithPrefixUpperCase(t *testing.T) { | ||
t.Parallel() | ||
|
||
for _, testcase := range []struct { | ||
name string | ||
env map[string]string | ||
file string | ||
args []string | ||
opts []ff.Option | ||
want fftest.Vars | ||
}{ | ||
{ | ||
name: "Parser with prefix", | ||
file: "testdata/prefix.env", | ||
want: fftest.Vars{S: "this", I: 1, F: 1.3, B: true, D: 10 * time.Second}, | ||
}, | ||
} { | ||
t.Run(testcase.name, func(t *testing.T) { | ||
if testcase.file != "" { | ||
testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithEnvVarSplit(","), ff.WithConfigFileParser(ffenv.ParserWithPrefix("TEST_PARSE"))) | ||
} | ||
|
||
if len(testcase.env) > 0 { | ||
for k, v := range testcase.env { | ||
defer os.Setenv(k, os.Getenv(k)) | ||
os.Setenv(k, v) | ||
} | ||
} | ||
|
||
fs, vars := fftest.Pair() | ||
vars.ParseError = ff.Parse(fs, testcase.args, testcase.opts...) | ||
t.Log("vars:", vars) | ||
if err := fftest.Compare(&testcase.want, vars); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
s=bar | ||
i=99 | ||
b=true | ||
d=1h |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
s=should be overridden | ||
|
||
d=3s |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
s=bar | ||
i=99 | ||
d=34s | ||
# comment line |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
s=from file | ||
i=200 # comment | ||
d=1m | ||
f=2.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
s=s.file.1 | ||
s=s.file.2 | ||
|
||
x=x.file.1 | ||
x=x.file.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
s=i=am=the=very=model=of=a=modern=major=general |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
TEST_PARSE_S=this | ||
TEST_PARSE_I=1 | ||
TEST_PARSE_F=1.3 | ||
TEST_PARSE_D=10s | ||
TEST_PARSE_B=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
b | ||
s=x |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
undef undefined variable | ||
s=one |