Skip to content

Commit 3cedc44

Browse files
authored
Load environment variables as a resolver (#480)
1 parent 6590294 commit 3cedc44

File tree

3 files changed

+69
-15
lines changed

3 files changed

+69
-15
lines changed

kong.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func New(grammar any, options ...Option) (*Kong, error) {
9191
},
9292
}
9393

94-
options = append(options, Bind(k))
94+
options = append(options, Bind(k), Resolvers(EnvResolver()))
9595

9696
for _, option := range options {
9797
if err := option.Apply(k); err != nil {

model.go

-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package kong
33
import (
44
"fmt"
55
"math"
6-
"os"
76
"reflect"
87
"strconv"
98
"strings"
@@ -377,19 +376,6 @@ func (v *Value) ApplyDefault() error {
377376
// Does not include resolvers.
378377
func (v *Value) Reset() error {
379378
v.Target.Set(reflect.Zero(v.Target.Type()))
380-
if len(v.Tag.Envs) != 0 {
381-
for _, env := range v.Tag.Envs {
382-
envar, ok := os.LookupEnv(env)
383-
// Parse the first non-empty ENV in the list
384-
if ok {
385-
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
386-
if err != nil {
387-
return fmt.Errorf("%s (from envar %s=%q)", err, env, envar)
388-
}
389-
return nil
390-
}
391-
}
392-
}
393379
if v.HasDefault {
394380
return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target)
395381
}

resolver.go

+68
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package kong
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"io"
7+
"os"
68
"strings"
79
)
810

@@ -66,3 +68,69 @@ func snakeCase(name string) string {
6668
name = strings.Join(strings.Split(strings.Title(name), "-"), "")
6769
return strings.ToLower(name[:1]) + name[1:]
6870
}
71+
72+
// EnvResolver provides a resolver for environment variables tags
73+
func EnvResolver() Resolver {
74+
// Resolvers are typically only invoked for flags, as shown here:
75+
// https://github.com/alecthomas/kong/blob/v1.6.0/context.go#L567
76+
// However, environment variable annotations can also apply to arguments,
77+
// as demonstrated in this test:
78+
// https://github.com/alecthomas/kong/blob/v1.6.0/kong_test.go#L1226-L1244
79+
// To handle this, we ensure that arguments are resolved as well.
80+
// Since the resolution only needs to happen once, we use this boolean
81+
// to track whether the resolution process has already been performed.
82+
argsResolved := false
83+
return ResolverFunc(func(context *Context, parent *Path, flag *Flag) (interface{}, error) {
84+
if !argsResolved {
85+
if err := resolveArgs(context.Path); err != nil {
86+
return nil, err
87+
}
88+
// once resolved we do not want to run this anymore
89+
argsResolved = true
90+
}
91+
for _, env := range flag.Tag.Envs {
92+
envar, ok := os.LookupEnv(env)
93+
// Parse the first non-empty ENV in the list
94+
if ok {
95+
return envar, nil
96+
}
97+
}
98+
return nil, nil
99+
})
100+
}
101+
102+
func resolveArgs(paths []*Path) error {
103+
for _, path := range paths {
104+
if path.Command == nil {
105+
continue
106+
}
107+
for _, positional := range path.Command.Positional {
108+
if positional.Tag == nil {
109+
continue
110+
}
111+
if err := visitValue(positional); err != nil {
112+
return err
113+
}
114+
}
115+
if path.Command.Argument != nil {
116+
if err := visitValue(path.Command.Argument); err != nil {
117+
return err
118+
}
119+
}
120+
}
121+
return nil
122+
}
123+
124+
func visitValue(value *Value) error {
125+
for _, env := range value.Tag.Envs {
126+
envar, ok := os.LookupEnv(env)
127+
if !ok {
128+
continue
129+
}
130+
token := Token{Type: FlagValueToken, Value: envar}
131+
if err := value.Parse(ScanFromTokens(token), value.Target); err != nil {
132+
return fmt.Errorf("%s (from envar %s=%q)", err, env, envar)
133+
}
134+
}
135+
return nil
136+
}

0 commit comments

Comments
 (0)