Skip to content

Commit

Permalink
add cli package with envconfig parser
Browse files Browse the repository at this point in the history
  • Loading branch information
colinhoglund committed Feb 11, 2025
1 parent a33d034 commit d920e5d
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 6 deletions.
34 changes: 34 additions & 0 deletions cli/envconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cli

import (
"github.com/kelseyhightower/envconfig"
"github.com/spf13/pflag"
)

// EnvconfigProcessWithPflags can be used to run envconfig.Process without stomping on flags
// passed to the command line, since explicit flags should take precedence over the environment.
func EnvconfigProcessWithPflags(prefix string, flags *pflag.FlagSet, obj any) error {
// discover all non-default flags before processing envconfig
var changedFlags = map[string]string{}

flags.VisitAll(func(f *pflag.Flag) {
// only include non-default flags
if f.Changed {
changedFlags[f.Name] = f.Value.String()
}
})

// apply envconfig values
if err := envconfig.Process(prefix, obj); err != nil {
return err
}

// re-apply changed flags to override environment
for name, value := range changedFlags {
if err := flags.Lookup(name).Value.Set(value); err != nil {
return err
}
}

return nil
}
160 changes: 160 additions & 0 deletions cli/envconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package cli_test

import (
"os"
"testing"

"github.com/kanopy-platform/go-library/cli"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

type testCLI struct {
String string
Int int
SubString string `split_words:"true"`
OverrideFlagRoot string `split_words:"true"`
OverrideFlagSub string `split_words:"true"`
}

func (c *testCLI) RootCmd() *cobra.Command {
emptyRun := func(_ *cobra.Command, _ []string) {}

root := &cobra.Command{
Use: "root",
Run: emptyRun,
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return cli.EnvconfigProcessWithPflags("APP", cmd.Flags(), c)
},
}

root.PersistentFlags().StringVar(&c.String, "string", "default", "")
root.PersistentFlags().IntVar(&c.Int, "int", 1, "")
root.PersistentFlags().StringVar(&c.OverrideFlagRoot, "override", "root", "")

sub := &cobra.Command{Use: "sub", Run: emptyRun}
sub.Flags().StringVar(&c.SubString, "substring", "default", "")
sub.PersistentFlags().StringVar(&c.OverrideFlagSub, "override", "sub", "")

root.AddCommand(sub)

return root
}

// checks that the correct order of precedence is adhered to: flag > env > default
func TestPrecedence(t *testing.T) {
tests := []struct {
desc string
env map[string]string
args []string
want testCLI
}{
{
desc: "test root defaults",
want: testCLI{
String: "default",
Int: 1,
SubString: "default",
OverrideFlagRoot: "root",
OverrideFlagSub: "sub",
},
},
{
desc: "test root override",
args: []string{"--override=flag"},
want: testCLI{
String: "default",
Int: 1,
SubString: "default",
OverrideFlagRoot: "flag",
OverrideFlagSub: "sub",
},
},
{
desc: "test sub defaults",
args: []string{"sub"},
want: testCLI{
String: "default",
Int: 1,
SubString: "default",
OverrideFlagRoot: "root",
OverrideFlagSub: "sub",
},
},
{
desc: "test sub env",
args: []string{"sub"},
env: map[string]string{
"APP_STRING": "env",
"APP_INT": "2",
"APP_SUB_STRING": "env",
"APP_OVERRIDE_FLAG_SUB": "env",
},
want: testCLI{
String: "env",
Int: 2,
SubString: "env",
OverrideFlagRoot: "root",
OverrideFlagSub: "env",
},
},
{
desc: "test sub flags",
args: []string{
"sub",
"--string=flag",
"--int=3",
"--substring=flag",
"--override=flag",
},
want: testCLI{
String: "flag",
Int: 3,
SubString: "flag",
OverrideFlagRoot: "root",
OverrideFlagSub: "flag",
},
},
{
desc: "test sub flags > env",
env: map[string]string{
"APP_STRING": "env",
"APP_INT": "2",
"APP_SUB_STRING": "env",
"APP_OVERRIDE_FLAG_ROOT": "env",
"APP_OVERRIDE_FLAG_SUB": "env",
},
args: []string{
"sub",
"--string=flag",
"--int=3",
"--substring=flag",
"--override=flag",
},
want: testCLI{
String: "flag",
Int: 3,
SubString: "flag",
OverrideFlagRoot: "env",
OverrideFlagSub: "flag",
},
},
}

for _, test := range tests {
for k, v := range test.env {
assert.NoError(t, os.Setenv(k, v))
}

cli := &testCLI{}
cmd := cli.RootCmd()
cmd.SetArgs(test.args)
assert.NoError(t, cmd.Execute())

assert.Equal(t, test.want, *cli, test.desc)

for k := range test.env {
assert.NoError(t, os.Unsetenv(k))
}
}
}
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ module github.com/kanopy-platform/go-library
go 1.23.0

require (
github.com/kelseyhightower/envconfig v1.4.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
golang.org/x/sys v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
20 changes: 17 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down

0 comments on commit d920e5d

Please sign in to comment.