Skip to content

Commit 3808437

Browse files
authored
Merge pull request #7 from kanopy-platform/add_cli_pkg
Add `cli` package with envconfig parser
2 parents 37d52fd + 1d294c6 commit 3808437

File tree

5 files changed

+207
-3
lines changed

5 files changed

+207
-3
lines changed

Makefile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ license-check:
1010
licensed cache
1111
licensed status
1212

13-
14-
1513
.PHONY: help
1614
help:
1715
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

cli/envconfig.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cli
2+
3+
import (
4+
"github.com/kelseyhightower/envconfig"
5+
"github.com/spf13/pflag"
6+
)
7+
8+
// EnvconfigProcessWithPflags can be used to run envconfig.Process without stomping on flags
9+
// passed to the command line, since explicit flags should take precedence over the environment.
10+
func EnvconfigProcessWithPflags(prefix string, flags *pflag.FlagSet, obj any) error {
11+
// discover all non-default flags before processing envconfig
12+
var changedFlags = map[string]string{}
13+
14+
flags.VisitAll(func(f *pflag.Flag) {
15+
// only include non-default flags
16+
if f.Changed {
17+
changedFlags[f.Name] = f.Value.String()
18+
}
19+
})
20+
21+
// apply envconfig values
22+
if err := envconfig.Process(prefix, obj); err != nil {
23+
return err
24+
}
25+
26+
// re-apply changed flags to override environment
27+
for name, value := range changedFlags {
28+
if err := flags.Lookup(name).Value.Set(value); err != nil {
29+
return err
30+
}
31+
}
32+
33+
return nil
34+
}

cli/envconfig_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package cli_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/kanopy-platform/go-library/cli"
8+
"github.com/spf13/cobra"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
type testCLI struct {
13+
String string
14+
Int int
15+
SubString string `split_words:"true"`
16+
OverrideFlagRoot string `split_words:"true"`
17+
OverrideFlagSub string `split_words:"true"`
18+
}
19+
20+
func (c *testCLI) RootCmd() *cobra.Command {
21+
emptyRun := func(_ *cobra.Command, _ []string) {}
22+
23+
root := &cobra.Command{
24+
Use: "root",
25+
Run: emptyRun,
26+
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
27+
return cli.EnvconfigProcessWithPflags("APP", cmd.Flags(), c)
28+
},
29+
}
30+
31+
root.PersistentFlags().StringVar(&c.String, "string", "default", "")
32+
root.PersistentFlags().IntVar(&c.Int, "int", 1, "")
33+
root.PersistentFlags().StringVar(&c.OverrideFlagRoot, "override", "root", "")
34+
35+
sub := &cobra.Command{Use: "sub", Run: emptyRun}
36+
sub.Flags().StringVar(&c.SubString, "substring", "default", "")
37+
sub.PersistentFlags().StringVar(&c.OverrideFlagSub, "override", "sub", "")
38+
39+
root.AddCommand(sub)
40+
41+
return root
42+
}
43+
44+
// checks that the correct order of precedence is adhered to: flag > env > default
45+
func TestPrecedence(t *testing.T) {
46+
tests := []struct {
47+
desc string
48+
env map[string]string
49+
args []string
50+
want testCLI
51+
}{
52+
{
53+
desc: "test root defaults",
54+
want: testCLI{
55+
String: "default",
56+
Int: 1,
57+
SubString: "default",
58+
OverrideFlagRoot: "root",
59+
OverrideFlagSub: "sub",
60+
},
61+
},
62+
{
63+
desc: "test root override",
64+
args: []string{"--override=flag"},
65+
want: testCLI{
66+
String: "default",
67+
Int: 1,
68+
SubString: "default",
69+
OverrideFlagRoot: "flag",
70+
OverrideFlagSub: "sub",
71+
},
72+
},
73+
{
74+
desc: "test sub defaults",
75+
args: []string{"sub"},
76+
want: testCLI{
77+
String: "default",
78+
Int: 1,
79+
SubString: "default",
80+
OverrideFlagRoot: "root",
81+
OverrideFlagSub: "sub",
82+
},
83+
},
84+
{
85+
desc: "test sub env",
86+
args: []string{"sub"},
87+
env: map[string]string{
88+
"APP_STRING": "env",
89+
"APP_INT": "2",
90+
"APP_SUB_STRING": "env",
91+
"APP_OVERRIDE_FLAG_SUB": "env",
92+
},
93+
want: testCLI{
94+
String: "env",
95+
Int: 2,
96+
SubString: "env",
97+
OverrideFlagRoot: "root",
98+
OverrideFlagSub: "env",
99+
},
100+
},
101+
{
102+
desc: "test sub flags",
103+
args: []string{
104+
"sub",
105+
"--string=flag",
106+
"--int=3",
107+
"--substring=flag",
108+
"--override=flag",
109+
},
110+
want: testCLI{
111+
String: "flag",
112+
Int: 3,
113+
SubString: "flag",
114+
OverrideFlagRoot: "root",
115+
OverrideFlagSub: "flag",
116+
},
117+
},
118+
{
119+
desc: "test sub flags > env",
120+
env: map[string]string{
121+
"APP_STRING": "env",
122+
"APP_INT": "2",
123+
"APP_SUB_STRING": "env",
124+
"APP_OVERRIDE_FLAG_ROOT": "env",
125+
"APP_OVERRIDE_FLAG_SUB": "env",
126+
},
127+
args: []string{
128+
"sub",
129+
"--string=flag",
130+
"--int=3",
131+
"--substring=flag",
132+
"--override=flag",
133+
},
134+
want: testCLI{
135+
String: "flag",
136+
Int: 3,
137+
SubString: "flag",
138+
OverrideFlagRoot: "env",
139+
OverrideFlagSub: "flag",
140+
},
141+
},
142+
}
143+
144+
for _, test := range tests {
145+
for k, v := range test.env {
146+
assert.NoError(t, os.Setenv(k, v))
147+
}
148+
149+
cli := &testCLI{}
150+
cmd := cli.RootCmd()
151+
cmd.SetArgs(test.args)
152+
assert.NoError(t, cmd.Execute())
153+
154+
assert.Equal(t, test.want, *cli, test.desc)
155+
156+
for k := range test.env {
157+
assert.NoError(t, os.Unsetenv(k))
158+
}
159+
}
160+
}

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ go 1.23.0
44

55
require (
66
github.com/go-jose/go-jose/v4 v4.0.4
7+
github.com/kelseyhightower/envconfig v1.4.0
78
github.com/okta/okta-sdk-golang/v5 v5.0.4
89
github.com/sirupsen/logrus v1.9.3
10+
github.com/spf13/cobra v1.8.1
11+
github.com/spf13/pflag v1.0.6
912
github.com/stretchr/testify v1.10.0
1013
)
1114

@@ -17,7 +20,7 @@ require (
1720
github.com/goccy/go-json v0.10.2 // indirect
1821
github.com/golang/protobuf v1.4.2 // indirect
1922
github.com/google/uuid v1.6.0 // indirect
20-
github.com/kelseyhightower/envconfig v1.4.0 // indirect
23+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2124
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
2225
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
2326
github.com/lestrrat-go/httpcc v1.0.1 // indirect

go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
4141
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
4242
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
4343
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
44+
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
4445
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4546
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4647
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -114,6 +115,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
114115
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
115116
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
116117
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
118+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
119+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
117120
github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc=
118121
github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk=
119122
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -149,8 +152,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
149152
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
150153
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
151154
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
155+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
152156
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
153157
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
158+
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
159+
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
160+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
161+
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
162+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
154163
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
155164
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
156165
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

0 commit comments

Comments
 (0)