From 9d4466f9319afdc971967768659329b3771f3d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Mon, 15 Jun 2020 02:17:14 +0200 Subject: [PATCH 01/12] add .env file support --- ffenv/env.go | 57 +++++++ ffenv/env_test.go | 296 +++++++++++++++++++++++++++++++++++ ffenv/testdata/1.env | 4 + ffenv/testdata/2.env | 4 + ffenv/testdata/3.env | 4 + ffenv/testdata/4.env | 4 + ffenv/testdata/5.env | 5 + ffenv/testdata/equals.env | 1 + ffenv/testdata/prefix.env | 5 + ffenv/testdata/solo_bool.env | 2 + ffenv/testdata/undefined.env | 2 + 11 files changed, 384 insertions(+) create mode 100644 ffenv/env.go create mode 100644 ffenv/env_test.go create mode 100644 ffenv/testdata/1.env create mode 100644 ffenv/testdata/2.env create mode 100644 ffenv/testdata/3.env create mode 100644 ffenv/testdata/4.env create mode 100644 ffenv/testdata/5.env create mode 100644 ffenv/testdata/equals.env create mode 100644 ffenv/testdata/prefix.env create mode 100644 ffenv/testdata/solo_bool.env create mode 100644 ffenv/testdata/undefined.env diff --git a/ffenv/env.go b/ffenv/env.go new file mode 100644 index 0000000..c9c8329 --- /dev/null +++ b/ffenv/env.go @@ -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) + } +} diff --git a/ffenv/env_test.go b/ffenv/env_test.go new file mode 100644 index 0000000..e971627 --- /dev/null +++ b/ffenv/env_test.go @@ -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) + } + }) + } +} diff --git a/ffenv/testdata/1.env b/ffenv/testdata/1.env new file mode 100644 index 0000000..5779214 --- /dev/null +++ b/ffenv/testdata/1.env @@ -0,0 +1,4 @@ +s=bar +i=99 +b=true +d=1h diff --git a/ffenv/testdata/2.env b/ffenv/testdata/2.env new file mode 100644 index 0000000..9d07afc --- /dev/null +++ b/ffenv/testdata/2.env @@ -0,0 +1,4 @@ + +s=should be overridden + +d=3s diff --git a/ffenv/testdata/3.env b/ffenv/testdata/3.env new file mode 100644 index 0000000..1b1ecf9 --- /dev/null +++ b/ffenv/testdata/3.env @@ -0,0 +1,4 @@ +s=bar +i=99 +d=34s +# comment line diff --git a/ffenv/testdata/4.env b/ffenv/testdata/4.env new file mode 100644 index 0000000..0b2a9af --- /dev/null +++ b/ffenv/testdata/4.env @@ -0,0 +1,4 @@ +s=from file +i=200 # comment +d=1m +f=2.3 diff --git a/ffenv/testdata/5.env b/ffenv/testdata/5.env new file mode 100644 index 0000000..bda3679 --- /dev/null +++ b/ffenv/testdata/5.env @@ -0,0 +1,5 @@ +s=s.file.1 +s=s.file.2 + +x=x.file.1 +x=x.file.2 diff --git a/ffenv/testdata/equals.env b/ffenv/testdata/equals.env new file mode 100644 index 0000000..3a4539d --- /dev/null +++ b/ffenv/testdata/equals.env @@ -0,0 +1 @@ +s=i=am=the=very=model=of=a=modern=major=general diff --git a/ffenv/testdata/prefix.env b/ffenv/testdata/prefix.env new file mode 100644 index 0000000..accd25f --- /dev/null +++ b/ffenv/testdata/prefix.env @@ -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 diff --git a/ffenv/testdata/solo_bool.env b/ffenv/testdata/solo_bool.env new file mode 100644 index 0000000..8ceb028 --- /dev/null +++ b/ffenv/testdata/solo_bool.env @@ -0,0 +1,2 @@ +b +s=x diff --git a/ffenv/testdata/undefined.env b/ffenv/testdata/undefined.env new file mode 100644 index 0000000..0b768b2 --- /dev/null +++ b/ffenv/testdata/undefined.env @@ -0,0 +1,2 @@ +undef undefined variable +s=one From 608ad3503aa661000df52c5b8cdbe47b0e828ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Fri, 24 Jul 2020 10:25:54 +0200 Subject: [PATCH 02/12] improve doc Co-authored-by: Peter Bourgon --- ffenv/env.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ffenv/env.go b/ffenv/env.go index c9c8329..02ae088 100644 --- a/ffenv/env.go +++ b/ffenv/env.go @@ -48,8 +48,9 @@ func parse(prefix string, r io.Reader, set func(name, value string) error) error return nil } -// ParserWithPrefix removes any prefix_ on keys in a .env file. -// MY_APP_PREFIX_KEY=value will get evaluated as key=value. +// ParserWithPrefix returns a Parser that will remove any prefix on keys in an +// .env file. For example, given prefix "MY_APP", the line `MY_APP_FOO=bar` +// in an .env file will be evaluated as name=foo, value=bar. 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) From 7de1992ef3052298974fd4e6d50692fbfdd81736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Fri, 24 Jul 2020 10:40:17 +0200 Subject: [PATCH 03/12] only support name=value format, event for boolean fixes: https://github.com/peterbourgon/ff/pull/66#pullrequestreview-454175131 --- ffenv/env.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ffenv/env.go b/ffenv/env.go index 02ae088..78eda30 100644 --- a/ffenv/env.go +++ b/ffenv/env.go @@ -2,6 +2,7 @@ package ffenv import ( "bufio" + "fmt" "io" "strings" ) @@ -31,12 +32,12 @@ func parse(prefix string, r io.Reader, set func(name, value string) error) error 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, "_", "-") + return fmt.Errorf("wrong format in env file, must be: name=value") } + 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]) } From 9af616575e1a15f91ecd0473263fa3f1d2101734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Fri, 25 Sep 2020 01:52:06 +0200 Subject: [PATCH 04/12] update test to be in line with new test from ff.PlainParser --- ffenv/env_test.go | 101 +++-------------------------------- ffenv/testdata/1.env | 8 +-- ffenv/testdata/solo_bool.env | 1 - ffenv/testdata/undefined.env | 2 +- 4 files changed, 11 insertions(+), 101 deletions(-) diff --git a/ffenv/env_test.go b/ffenv/env_test.go index e971627..c438863 100644 --- a/ffenv/env_test.go +++ b/ffenv/env_test.go @@ -83,11 +83,6 @@ func TestParseBasics(t *testing.T) { 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", @@ -137,8 +132,9 @@ func TestParseBasics(t *testing.T) { }, } { t.Run(testcase.name, func(t *testing.T) { + t.Log("%%%%", testcase.name) if testcase.file != "" { - testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.Parser)) + testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.Parser(t))) } if len(testcase.env) > 0 { @@ -150,6 +146,7 @@ func TestParseBasics(t *testing.T) { 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) } @@ -182,12 +179,12 @@ func TestParseIssue16(t *testing.T) { }, { name: "only comment with space", - data: "# foo=bar\n", + data: "# foo bar\n", want: "", }, { name: "only comment no space", - data: "#foo=bar\n", + data: "#foo bar\n", want: "", }, } { @@ -198,7 +195,7 @@ func TestParseIssue16(t *testing.T) { fs, vars := fftest.Pair() vars.ParseError = ff.Parse(fs, []string{}, ff.WithConfigFile(filename), - ff.WithConfigFileParser(ffenv.Parser), + ff.WithConfigFileParser(ffenv.Parser(t)), ) want := fftest.Vars{S: testcase.want} @@ -208,89 +205,3 @@ func TestParseIssue16(t *testing.T) { }) } } - -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) - } - }) - } -} diff --git a/ffenv/testdata/1.env b/ffenv/testdata/1.env index 5779214..b0c224a 100644 --- a/ffenv/testdata/1.env +++ b/ffenv/testdata/1.env @@ -1,4 +1,4 @@ -s=bar -i=99 -b=true -d=1h +TEST_PARSE_S=bar +TEST_PARSE_I=99 +TEST_PARSE_B=true +TEST_PARSE_D=1h diff --git a/ffenv/testdata/solo_bool.env b/ffenv/testdata/solo_bool.env index 8ceb028..8381d83 100644 --- a/ffenv/testdata/solo_bool.env +++ b/ffenv/testdata/solo_bool.env @@ -1,2 +1 @@ -b s=x diff --git a/ffenv/testdata/undefined.env b/ffenv/testdata/undefined.env index 0b768b2..b60db8b 100644 --- a/ffenv/testdata/undefined.env +++ b/ffenv/testdata/undefined.env @@ -1,2 +1,2 @@ -undef undefined variable +undef=undefined variable s=one From 7c23ab369ab958c8109e706fab041b2dee9852db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Fri, 25 Sep 2020 02:26:06 +0200 Subject: [PATCH 05/12] merge debug with testing.T.Log and parsing with env var prefix --- ffenv/env.go | 92 ++++++++++++++++++++++++++++++----------------- ffenv/env_test.go | 57 ++++++++++++++++++++++++++--- testdata/1.conf | 8 ++--- 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/ffenv/env.go b/ffenv/env.go index 78eda30..cc5f924 100644 --- a/ffenv/env.go +++ b/ffenv/env.go @@ -2,58 +2,86 @@ package ffenv import ( "bufio" + "flag" "fmt" "io" "strings" + "testing" ) // 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 Parser(fs *flag.FlagSet, t *testing.T) func(io.Reader, func(string, string) error) error { + return func(r io.Reader, set func(name, value string) error) error { + return parse(fs, t)("", 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 - } +// Parser is a parser for config files in an extremely simple format. Each +// line is tokenized as a single key/value pair. The first =-delimited +// token in the line is interpreted as the flag name, and all remaining tokens +// are interpreted as the value. Any leading hyphens on the flag name are +// ignored. +func parse(fs *flag.FlagSet, t *testing.T) func(string, io.Reader, func(string, string) error) error { + return func(prefix string, r io.Reader, set func(name, value string) error) error { - if line[0] == '#' { - continue // skip comments - } + var flags []string + fs.VisitAll(func(f *flag.Flag) { + flags = append(flags, f.Name) + }) - line = strings.TrimPrefix(line, prefix+"_") - var ( - name string - value string - index = strings.IndexRune(line, '=') - ) - if index < 0 { - return fmt.Errorf("wrong format in env file, must be: name=value") - } + s := bufio.NewScanner(r) + for s.Scan() { + line := strings.TrimSpace(s.Text()) + if line == "" { + continue // skip empties + } - name, value = strings.ToLower(line[:index]), line[index+1:] - name = strings.ReplaceAll(name, "_", "-") + if line[0] == '#' { + continue // skip comments + } - if i := strings.Index(value, " #"); i >= 0 { - value = strings.TrimSpace(value[:i]) - } + line = strings.TrimPrefix(line, prefix+"_") + + var ( + name string + value string + index = strings.IndexRune(line, '=') + ) + if index < 0 { + return fmt.Errorf("wrong format in env file, must be: name=value") + } + + name, value = strings.ToLower(line[:index]), line[index+1:] + + if i := strings.Index(value, " #"); i >= 0 { + value = strings.TrimSpace(value[:i]) + } + + for _, sep := range []string{"-", ".", "/"} { + for _, f := range flags { + t.Log("YO: line", line, "flags", f, "name", name, "value", value) + if f == strings.ReplaceAll(name, "_", sep) { + t.Log("YO2: line", line, "flags", f, "name", name, "value", value) + if err := set(name, value); err != nil { + t.Log("NOK") + return err + } + break + } + } + } - if err := set(name, value); err != nil { - return err } + return nil } - return nil } // ParserWithPrefix returns a Parser that will remove any prefix on keys in an -// .env file. For example, given prefix "MY_APP", the line `MY_APP_FOO=bar` +// .env file. For example, given prefix "MY_APP", the line `MY_APP_FOO=bar` // in an .env file will be evaluated as name=foo, value=bar. -func ParserWithPrefix(prefix string) func(io.Reader, func(string, string) error) error { +func ParserWithPrefix(prefix string, fs *flag.FlagSet, t *testing.T) 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) + return parse(fs, t)(prefix, r, set) } } diff --git a/ffenv/env_test.go b/ffenv/env_test.go index c438863..0eb760f 100644 --- a/ffenv/env_test.go +++ b/ffenv/env_test.go @@ -133,8 +133,9 @@ func TestParseBasics(t *testing.T) { } { t.Run(testcase.name, func(t *testing.T) { t.Log("%%%%", testcase.name) + fs, vars := fftest.Pair() if testcase.file != "" { - testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.Parser(t))) + testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.Parser(fs, t))) } if len(testcase.env) > 0 { @@ -144,7 +145,6 @@ func TestParseBasics(t *testing.T) { } } - 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 { @@ -179,12 +179,12 @@ func TestParseIssue16(t *testing.T) { }, { name: "only comment with space", - data: "# foo bar\n", + data: "#=foo=bar\n", want: "", }, { name: "only comment no space", - data: "#foo bar\n", + data: "#foo=bar\n", want: "", }, } { @@ -195,7 +195,7 @@ func TestParseIssue16(t *testing.T) { fs, vars := fftest.Pair() vars.ParseError = ff.Parse(fs, []string{}, ff.WithConfigFile(filename), - ff.WithConfigFileParser(ffenv.Parser(t)), + ff.WithConfigFileParser(ffenv.Parser(fs, t)), ) want := fftest.Vars{S: testcase.want} @@ -205,3 +205,50 @@ func TestParseIssue16(t *testing.T) { }) } } + +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() + } + + fs, vars := fftest.Pair() + options := []ff.Option{ff.WithConfigFile(filename), ff.WithConfigFileParser(ffenv.Parser(fs, t))} + if testcase.allowMissing { + options = append(options, ff.WithAllowMissingConfigFile(true)) + } + + vars.ParseError = ff.Parse(fs, []string{}, options...) + + want := fftest.Vars{WantParseErrorIs: testcase.parseError} + if err := fftest.Compare(&want, vars); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/testdata/1.conf b/testdata/1.conf index 48ff474..5779214 100644 --- a/testdata/1.conf +++ b/testdata/1.conf @@ -1,4 +1,4 @@ -s bar -i 99 -b true -d 1h \ No newline at end of file +s=bar +i=99 +b=true +d=1h From b011e1c6d39e95ef60ba7d381a971fa3a5c9d376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Fri, 25 Sep 2020 01:52:06 +0200 Subject: [PATCH 06/12] update test to be in line with new test from ff.PlainParser --- testdata/1.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testdata/1.conf b/testdata/1.conf index 5779214..867b455 100644 --- a/testdata/1.conf +++ b/testdata/1.conf @@ -1,4 +1,4 @@ -s=bar -i=99 -b=true -d=1h +s bar +i 99 +b true +d 1h From 26d885ca55ac8472f12d7a909fd7a0d262ec626a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Thu, 29 Oct 2020 00:38:27 +0100 Subject: [PATCH 07/12] add tests for long options --- ffenv/env.go | 7 ++++--- ffenv/env_test.go | 32 +++++++++++++------------------- fftest/vars.go | 32 ++++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/ffenv/env.go b/ffenv/env.go index cc5f924..16f7cac 100644 --- a/ffenv/env.go +++ b/ffenv/env.go @@ -61,17 +61,18 @@ func parse(fs *flag.FlagSet, t *testing.T) func(string, io.Reader, func(string, for _, sep := range []string{"-", ".", "/"} { for _, f := range flags { t.Log("YO: line", line, "flags", f, "name", name, "value", value) + // here we already check the existence of the flag, that's why the func set() cannot check it in ff.Parse if f == strings.ReplaceAll(name, "_", sep) { t.Log("YO2: line", line, "flags", f, "name", name, "value", value) if err := set(name, value); err != nil { t.Log("NOK") return err } - break + goto done } } } - + done: } return nil } @@ -80,7 +81,7 @@ func parse(fs *flag.FlagSet, t *testing.T) func(string, io.Reader, func(string, // ParserWithPrefix returns a Parser that will remove any prefix on keys in an // .env file. For example, given prefix "MY_APP", the line `MY_APP_FOO=bar` // in an .env file will be evaluated as name=foo, value=bar. -func ParserWithPrefix(prefix string, fs *flag.FlagSet, t *testing.T) func(io.Reader, func(string, string) error) error { +func ParserWithPrefix(fs *flag.FlagSet, prefix string, t *testing.T) func(io.Reader, func(string, string) error) error { return func(r io.Reader, set func(name, value string) error) error { return parse(fs, t)(prefix, r, set) } diff --git a/ffenv/env_test.go b/ffenv/env_test.go index 0eb760f..6cfcd8f 100644 --- a/ffenv/env_test.go +++ b/ffenv/env_test.go @@ -34,6 +34,7 @@ func TestParseBasics(t *testing.T) { { name: "file only", file: "testdata/1.env", + opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, want: fftest.Vars{S: "bar", I: 99, B: true, D: time.Hour}, }, { @@ -75,6 +76,11 @@ func TestParseBasics(t *testing.T) { 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: "long args", + args: []string{"-s_s", "f_oo", "-s-s", "f-oo", "-s.s", "f.oo", "-s/s", "f/oo"}, + want: fftest.Vars{S_S: "f_oo", SDashS: "f-oo", SDotS: "f.oo", SSlashS: "f/oo"}, + }, { name: "priority repeats", env: map[string]string{"TEST_PARSE_S": "s.env", "TEST_PARSE_X": "x.env.1"}, @@ -106,36 +112,24 @@ func TestParseBasics(t *testing.T) { 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 "}}, }, + { + name: "flags with .", + env: map[string]string{"TEST_PARSE_S_S": "one"}, + opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, + want: fftest.Vars{SDashS: "one", S_S: "one", SDotS: "one", SSlashS: "one"}, + }, } { t.Run(testcase.name, func(t *testing.T) { t.Log("%%%%", testcase.name) fs, vars := fftest.Pair() if testcase.file != "" { - testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.Parser(fs, t))) + testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.ParserWithPrefix(fs, "TEST_PARSE", t))) } if len(testcase.env) > 0 { diff --git a/fftest/vars.go b/fftest/vars.go index a181c8c..47fa273 100644 --- a/fftest/vars.go +++ b/fftest/vars.go @@ -18,6 +18,10 @@ func Pair() (*flag.FlagSet, *Vars) { var v Vars fs.StringVar(&v.S, "s", "", "string") + fs.StringVar(&v.S_S, "s_s", "", "string") + fs.StringVar(&v.SDashS, "s-s", "", "string") + fs.StringVar(&v.SDotS, "s.s", "", "string") + fs.StringVar(&v.SSlashS, "s/s", "", "string") fs.IntVar(&v.I, "i", 0, "int") fs.Float64Var(&v.F, "f", 0., "float64") fs.BoolVar(&v.B, "b", false, "bool") @@ -29,12 +33,16 @@ func Pair() (*flag.FlagSet, *Vars) { // Vars are a common set of variables used for testing. type Vars struct { - S string - I int - F float64 - B bool - D time.Duration - X StringSlice + S string + S_S string + SDotS string + SDashS string + SSlashS string + I int + F float64 + B bool + D time.Duration + X StringSlice // ParseError should be assigned as the result of Parse in tests. ParseError error @@ -80,6 +88,18 @@ func Compare(want, have *Vars) error { if want.S != have.S { return fmt.Errorf("var S: want %q, have %q", want.S, have.S) } + if want.SDashS != have.SDashS { + return fmt.Errorf("var SDashS: want %q, have %q", want.SDashS, have.SDashS) + } + if want.SDotS != have.SDotS { + return fmt.Errorf("var SDotS: want %q, have %q", want.SDotS, have.SDotS) + } + if want.SSlashS != have.SSlashS { + return fmt.Errorf("var SSlashS: want %q, have %q", want.SSlashS, have.SSlashS) + } + if want.S_S != have.S_S { + return fmt.Errorf("var S_S: want %q, have %q", want.S_S, have.S_S) + } if want.I != have.I { return fmt.Errorf("var I: want %d, have %d", want.I, have.I) } From d0439483a2e2387e44273e9138180c7b74288647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Thu, 29 Oct 2020 02:37:51 +0100 Subject: [PATCH 08/12] change logic flaw --- ffenv/env.go | 29 +++++++++++++++-------------- ffenv/env_test.go | 12 +++++------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/ffenv/env.go b/ffenv/env.go index 16f7cac..5230607 100644 --- a/ffenv/env.go +++ b/ffenv/env.go @@ -6,14 +6,13 @@ import ( "fmt" "io" "strings" - "testing" ) // Parser is a parser for .env file format: flag=value. Each // line is tokenized as a single key/value pair. -func Parser(fs *flag.FlagSet, t *testing.T) func(io.Reader, func(string, string) error) error { +func Parser(fs *flag.FlagSet) func(io.Reader, func(string, string) error) error { return func(r io.Reader, set func(name, value string) error) error { - return parse(fs, t)("", r, set) + return parse(fs)("", r, set) } } @@ -22,7 +21,7 @@ func Parser(fs *flag.FlagSet, t *testing.T) func(io.Reader, func(string, string) // token in the line is interpreted as the flag name, and all remaining tokens // are interpreted as the value. Any leading hyphens on the flag name are // ignored. -func parse(fs *flag.FlagSet, t *testing.T) func(string, io.Reader, func(string, string) error) error { +func parse(fs *flag.FlagSet) func(string, io.Reader, func(string, string) error) error { return func(prefix string, r io.Reader, set func(name, value string) error) error { var flags []string @@ -58,20 +57,22 @@ func parse(fs *flag.FlagSet, t *testing.T) func(string, io.Reader, func(string, value = strings.TrimSpace(value[:i]) } - for _, sep := range []string{"-", ".", "/"} { + var errs []error + for _, sep := range []string{"_", "-", ".", "/"} { for _, f := range flags { - t.Log("YO: line", line, "flags", f, "name", name, "value", value) - // here we already check the existence of the flag, that's why the func set() cannot check it in ff.Parse - if f == strings.ReplaceAll(name, "_", sep) { - t.Log("YO2: line", line, "flags", f, "name", name, "value", value) - if err := set(name, value); err != nil { - t.Log("NOK") - return err + replaced := strings.ReplaceAll(name, "_", sep) + if f == replaced { + if err := set(replaced, value); err != nil { + errs = append(errs, err) + continue } goto done } } } + if len(errs) > 0 { + return errs[0] + } done: } return nil @@ -81,8 +82,8 @@ func parse(fs *flag.FlagSet, t *testing.T) func(string, io.Reader, func(string, // ParserWithPrefix returns a Parser that will remove any prefix on keys in an // .env file. For example, given prefix "MY_APP", the line `MY_APP_FOO=bar` // in an .env file will be evaluated as name=foo, value=bar. -func ParserWithPrefix(fs *flag.FlagSet, prefix string, t *testing.T) func(io.Reader, func(string, string) error) error { +func ParserWithPrefix(fs *flag.FlagSet, prefix string) func(io.Reader, func(string, string) error) error { return func(r io.Reader, set func(name, value string) error) error { - return parse(fs, t)(prefix, r, set) + return parse(fs)(prefix, r, set) } } diff --git a/ffenv/env_test.go b/ffenv/env_test.go index 6cfcd8f..0240cd0 100644 --- a/ffenv/env_test.go +++ b/ffenv/env_test.go @@ -119,17 +119,16 @@ func TestParseBasics(t *testing.T) { want: fftest.Vars{S: " three ", X: []string{"one", " two", " three "}}, }, { - name: "flags with .", + name: "flags with . from env", env: map[string]string{"TEST_PARSE_S_S": "one"}, opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, - want: fftest.Vars{SDashS: "one", S_S: "one", SDotS: "one", SSlashS: "one"}, + want: fftest.Vars{S_S: "one", SDashS: "one", SDotS: "one", SSlashS: "one"}, }, } { t.Run(testcase.name, func(t *testing.T) { - t.Log("%%%%", testcase.name) fs, vars := fftest.Pair() if testcase.file != "" { - testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.ParserWithPrefix(fs, "TEST_PARSE", t))) + testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.ParserWithPrefix(fs, "TEST_PARSE"))) } if len(testcase.env) > 0 { @@ -140,7 +139,6 @@ func TestParseBasics(t *testing.T) { } 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) } @@ -189,7 +187,7 @@ func TestParseIssue16(t *testing.T) { fs, vars := fftest.Pair() vars.ParseError = ff.Parse(fs, []string{}, ff.WithConfigFile(filename), - ff.WithConfigFileParser(ffenv.Parser(fs, t)), + ff.WithConfigFileParser(ffenv.Parser(fs)), ) want := fftest.Vars{S: testcase.want} @@ -232,7 +230,7 @@ func TestParseConfigFile(t *testing.T) { } fs, vars := fftest.Pair() - options := []ff.Option{ff.WithConfigFile(filename), ff.WithConfigFileParser(ffenv.Parser(fs, t))} + options := []ff.Option{ff.WithConfigFile(filename), ff.WithConfigFileParser(ffenv.Parser(fs))} if testcase.allowMissing { options = append(options, ff.WithAllowMissingConfigFile(true)) } From c692a0a11a1ec553314e2dd647ea8d38b58f78fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Thu, 29 Oct 2020 23:17:26 +0100 Subject: [PATCH 09/12] forgot to update test with help usage --- ffcli/command_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ffcli/command_test.go b/ffcli/command_test.go index 2c31c63..83f5f03 100644 --- a/ffcli/command_test.go +++ b/ffcli/command_test.go @@ -548,5 +548,9 @@ FLAGS -f 0 float64 -i 0 int -s ... string + -s-s ... string + -s.s ... string + -s/s ... string + -s_s ... string -x ... collection of strings (repeatable) `) + "\n" From f9307cc33e2eefb3e4a0444715ae2a0ab661a8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Fri, 13 Nov 2020 03:14:55 +0100 Subject: [PATCH 10/12] deal with scanner.Err() --- ffenv/env.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffenv/env.go b/ffenv/env.go index 5230607..6dabb99 100644 --- a/ffenv/env.go +++ b/ffenv/env.go @@ -75,7 +75,7 @@ func parse(fs *flag.FlagSet) func(string, io.Reader, func(string, string) error) } done: } - return nil + return s.Err() } } From 6c60d05c54a84cf1c29b6c595cdbca4a66a9f036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Thu, 26 Nov 2020 23:26:58 +0100 Subject: [PATCH 11/12] add own test Vars type --- ffenv/env_test.go | 79 +++++++++++++++++++++++++++++++++++------------ fftest/vars.go | 32 ++++--------------- 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/ffenv/env_test.go b/ffenv/env_test.go index 0240cd0..a4c18c7 100644 --- a/ffenv/env_test.go +++ b/ffenv/env_test.go @@ -1,6 +1,8 @@ package ffenv_test import ( + "flag" + "fmt" "os" "testing" "time" @@ -10,6 +12,43 @@ import ( "github.com/peterbourgon/ff/v3/fftest" ) +type Vars struct { + *fftest.Vars + S_S string + SDotS string + SDashS string + SSlashS string +} + +func Pair() (*flag.FlagSet, *Vars) { + fs, v := fftest.Pair() + + var vv Vars + vv.Vars = v + + fs.StringVar(&vv.S_S, "s_s", "", "string") + fs.StringVar(&vv.SDashS, "s-s", "", "string") + fs.StringVar(&vv.SDotS, "s.s", "", "string") + fs.StringVar(&vv.SSlashS, "s/s", "", "string") + return fs, &vv +} + +func Compare(want, have *Vars) error { + if want.SDashS != have.SDashS { + return fmt.Errorf("var SDashS: want %q, have %q", want.SDashS, have.SDashS) + } + if want.SDotS != have.SDotS { + return fmt.Errorf("var SDotS: want %q, have %q", want.SDotS, have.SDotS) + } + if want.SSlashS != have.SSlashS { + return fmt.Errorf("var SSlashS: want %q, have %q", want.SSlashS, have.SSlashS) + } + if want.S_S != have.S_S { + return fmt.Errorf("var S_S: want %q, have %q", want.S_S, have.S_S) + } + return fftest.Compare(want.Vars, have.Vars) +} + func TestParseBasics(t *testing.T) { t.Parallel() @@ -19,49 +58,49 @@ func TestParseBasics(t *testing.T) { file string args []string opts []ff.Option - want fftest.Vars + want Vars }{ { name: "empty", args: []string{}, - want: fftest.Vars{}, + want: Vars{Vars: &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}, + want: Vars{Vars: &fftest.Vars{S: "foo", I: 123, B: true, D: 24 * time.Minute}}, }, { name: "file only", file: "testdata/1.env", opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, - want: fftest.Vars{S: "bar", I: 99, B: true, D: time.Hour}, + want: Vars{Vars: &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}, + want: Vars{Vars: &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}, + want: Vars{Vars: &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}, + want: Vars{Vars: &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}, + want: Vars{Vars: &fftest.Vars{S: "env takes priority", I: 99, B: true, D: 34 * time.Second}}, }, { name: "file env args", @@ -69,17 +108,17 @@ func TestParseBasics(t *testing.T) { 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}, + want: Vars{Vars: &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"}}, + want: Vars{Vars: &fftest.Vars{S: "bar", D: time.Hour, X: []string{"1", "2", "3"}}}, }, { name: "long args", args: []string{"-s_s", "f_oo", "-s-s", "f-oo", "-s.s", "f.oo", "-s/s", "f/oo"}, - want: fftest.Vars{S_S: "f_oo", SDashS: "f-oo", SDotS: "f.oo", SSlashS: "f/oo"}, + want: Vars{S_S: "f_oo", SDashS: "f-oo", SDotS: "f.oo", SSlashS: "f/oo", Vars: &fftest.Vars{}}, }, { name: "priority repeats", @@ -87,46 +126,46 @@ func TestParseBasics(t *testing.T) { 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 + want: Vars{Vars: &fftest.Vars{S: "s.arg.2", X: []string{"x.arg.1", "x.arg.2"}}}, // highest prio wins and no others are called }, { name: "PlainParser string with spaces", file: "testdata/equals.env", - want: fftest.Vars{S: "i=am=the=very=model=of=a=modern=major=general"}, + want: Vars{Vars: &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"}}, + want: Vars{Vars: &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"}}, + want: Vars{Vars: &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"}, + want: Vars{Vars: &fftest.Vars{S: "bar"}}, }, { 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 "}}, + want: Vars{Vars: &fftest.Vars{S: " three ", X: []string{"one", " two", " three "}}}, }, { name: "flags with . from env", env: map[string]string{"TEST_PARSE_S_S": "one"}, opts: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE")}, - want: fftest.Vars{S_S: "one", SDashS: "one", SDotS: "one", SSlashS: "one"}, + want: Vars{S_S: "one", SDashS: "one", SDotS: "one", SSlashS: "one", Vars: &fftest.Vars{}}, }, } { t.Run(testcase.name, func(t *testing.T) { - fs, vars := fftest.Pair() + fs, vars := Pair() if testcase.file != "" { testcase.opts = append(testcase.opts, ff.WithConfigFile(testcase.file), ff.WithConfigFileParser(ffenv.ParserWithPrefix(fs, "TEST_PARSE"))) } @@ -139,7 +178,7 @@ func TestParseBasics(t *testing.T) { } vars.ParseError = ff.Parse(fs, testcase.args, testcase.opts...) - if err := fftest.Compare(&testcase.want, vars); err != nil { + if err := Compare(&testcase.want, vars); err != nil { t.Fatal(err) } }) diff --git a/fftest/vars.go b/fftest/vars.go index 47fa273..a181c8c 100644 --- a/fftest/vars.go +++ b/fftest/vars.go @@ -18,10 +18,6 @@ func Pair() (*flag.FlagSet, *Vars) { var v Vars fs.StringVar(&v.S, "s", "", "string") - fs.StringVar(&v.S_S, "s_s", "", "string") - fs.StringVar(&v.SDashS, "s-s", "", "string") - fs.StringVar(&v.SDotS, "s.s", "", "string") - fs.StringVar(&v.SSlashS, "s/s", "", "string") fs.IntVar(&v.I, "i", 0, "int") fs.Float64Var(&v.F, "f", 0., "float64") fs.BoolVar(&v.B, "b", false, "bool") @@ -33,16 +29,12 @@ func Pair() (*flag.FlagSet, *Vars) { // Vars are a common set of variables used for testing. type Vars struct { - S string - S_S string - SDotS string - SDashS string - SSlashS string - I int - F float64 - B bool - D time.Duration - X StringSlice + S string + I int + F float64 + B bool + D time.Duration + X StringSlice // ParseError should be assigned as the result of Parse in tests. ParseError error @@ -88,18 +80,6 @@ func Compare(want, have *Vars) error { if want.S != have.S { return fmt.Errorf("var S: want %q, have %q", want.S, have.S) } - if want.SDashS != have.SDashS { - return fmt.Errorf("var SDashS: want %q, have %q", want.SDashS, have.SDashS) - } - if want.SDotS != have.SDotS { - return fmt.Errorf("var SDotS: want %q, have %q", want.SDotS, have.SDotS) - } - if want.SSlashS != have.SSlashS { - return fmt.Errorf("var SSlashS: want %q, have %q", want.SSlashS, have.SSlashS) - } - if want.S_S != have.S_S { - return fmt.Errorf("var S_S: want %q, have %q", want.S_S, have.S_S) - } if want.I != have.I { return fmt.Errorf("var I: want %d, have %d", want.I, have.I) } From 1c35eb90e39406c48d388dd8d030418ba7b72dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20=E2=A7=93=20Herrmann?= Date: Thu, 26 Nov 2020 23:53:59 +0100 Subject: [PATCH 12/12] remove reference test vars from ffenv in ffcli (usage) --- ffcli/command_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ffcli/command_test.go b/ffcli/command_test.go index 83f5f03..2c31c63 100644 --- a/ffcli/command_test.go +++ b/ffcli/command_test.go @@ -548,9 +548,5 @@ FLAGS -f 0 float64 -i 0 int -s ... string - -s-s ... string - -s.s ... string - -s/s ... string - -s_s ... string -x ... collection of strings (repeatable) `) + "\n"