feat: support multiple env variables (#349)
This commit is contained in:
@@ -522,10 +522,10 @@ Tags can be in two forms:
|
||||
Both can coexist with standard Tag parsing.
|
||||
|
||||
| Tag | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `cmd:""` | If present, struct is a command. |
|
||||
| `arg:""` | If present, field is an argument. Required by default. |
|
||||
| `env:"X"` | Specify envar to use for default value. |
|
||||
| `env:"X,Y,..."` | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used. |
|
||||
| `name:"X"` | Long name, for overriding field name. |
|
||||
| `help:"X"` | Help text. |
|
||||
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
|
||||
|
||||
@@ -138,8 +138,10 @@ MAIN:
|
||||
name = tag.Prefix + name
|
||||
}
|
||||
|
||||
if tag.Env != "" {
|
||||
tag.Env = tag.EnvPrefix + tag.Env
|
||||
if len(tag.Envs) != 0 {
|
||||
for i := range tag.Envs {
|
||||
tag.Envs[i] = tag.EnvPrefix + tag.Envs[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Nested structs are either commands or args, unless they implement the Mapper interface.
|
||||
@@ -304,7 +306,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
|
||||
Value: value,
|
||||
Short: tag.Short,
|
||||
PlaceHolder: tag.PlaceHolder,
|
||||
Env: tag.Env,
|
||||
Envs: tag.Envs,
|
||||
Group: buildGroupForKey(k, tag.Group),
|
||||
Xor: tag.Xor,
|
||||
Hidden: tag.Hidden,
|
||||
|
||||
+15
-7
@@ -165,16 +165,16 @@ func (c *Context) Validate() error { // nolint: gocyclo
|
||||
err := Visit(c.Model, func(node Visitable, next Next) error {
|
||||
switch node := node.(type) {
|
||||
case *Value:
|
||||
_, ok := os.LookupEnv(node.Tag.Env)
|
||||
if node.Enum != "" && (!node.Required || node.HasDefault || (node.Tag.Env != "" && ok)) {
|
||||
ok := atLeastOneEnvSet(node.Tag.Envs)
|
||||
if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) {
|
||||
if err := checkEnum(node, node.Target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case *Flag:
|
||||
_, ok := os.LookupEnv(node.Tag.Env)
|
||||
if node.Enum != "" && (!node.Required || node.HasDefault || (node.Tag.Env != "" && ok)) {
|
||||
ok := atLeastOneEnvSet(node.Tag.Envs)
|
||||
if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) {
|
||||
if err := checkEnum(node.Value, node.Target); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -890,9 +890,8 @@ func checkMissingPositionals(positional int, values []*Value) error {
|
||||
for ; positional < len(values); positional++ {
|
||||
arg := values[positional]
|
||||
// TODO(aat): Fix hardcoding of these env checks all over the place :\
|
||||
if arg.Tag.Env != "" {
|
||||
_, ok := os.LookupEnv(arg.Tag.Env)
|
||||
if ok {
|
||||
if len(arg.Tag.Envs) != 0 {
|
||||
if atLeastOneEnvSet(arg.Tag.Envs) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -997,3 +996,12 @@ func isValidatable(v reflect.Value) validatable {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func atLeastOneEnvSet(envs []string) bool {
|
||||
for _, env := range envs {
|
||||
if _, ok := os.LookupEnv(env); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ type HelpValueFormatter func(value *Value) string
|
||||
|
||||
// DefaultHelpValueFormatter is the default HelpValueFormatter.
|
||||
func DefaultHelpValueFormatter(value *Value) string {
|
||||
if value.Tag.Env == "" || HasInterpolatedVar(value.OrigHelp, "env") {
|
||||
if len(value.Tag.Envs) == 0 || HasInterpolatedVar(value.OrigHelp, "env") {
|
||||
return value.Help
|
||||
}
|
||||
suffix := "($" + value.Tag.Env + ")"
|
||||
suffix := "(" + formatEnvs(value.Tag.Envs) + ")"
|
||||
switch {
|
||||
case strings.HasSuffix(value.Help, "."):
|
||||
return value.Help[:len(value.Help)-1] + " " + suffix + "."
|
||||
@@ -567,3 +567,12 @@ func TreeIndenter(prefix string) string {
|
||||
}
|
||||
return "|" + strings.Repeat(" ", defaultIndent) + prefix
|
||||
}
|
||||
|
||||
func formatEnvs(envs []string) string {
|
||||
formatted := make([]string, len(envs))
|
||||
for i := range envs {
|
||||
formatted[i] = "$" + envs[i]
|
||||
}
|
||||
|
||||
return strings.Join(formatted, ", ")
|
||||
}
|
||||
|
||||
@@ -472,6 +472,18 @@ func TestEnvarAutoHelp(t *testing.T) {
|
||||
assert.Contains(t, w.String(), "A flag ($FLAG).")
|
||||
}
|
||||
|
||||
func TestMultipleEnvarAutoHelp(t *testing.T) {
|
||||
var cli struct {
|
||||
Flag string `env:"FLAG1,FLAG2" help:"A flag."`
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
|
||||
_, err := p.Parse([]string{"--help"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, w.String(), "A flag ($FLAG1, $FLAG2).")
|
||||
}
|
||||
|
||||
//nolint:dupl // false positive
|
||||
func TestEnvarAutoHelpWithEnvPrefix(t *testing.T) {
|
||||
type Anonymous struct {
|
||||
Flag string `env:"FLAG" help:"A flag."`
|
||||
@@ -488,6 +500,24 @@ func TestEnvarAutoHelpWithEnvPrefix(t *testing.T) {
|
||||
assert.Contains(t, w.String(), "A different flag.")
|
||||
}
|
||||
|
||||
//nolint:dupl // false positive
|
||||
func TestMultipleEnvarAutoHelpWithEnvPrefix(t *testing.T) {
|
||||
type Anonymous struct {
|
||||
Flag string `env:"FLAG1,FLAG2" help:"A flag."`
|
||||
Other string `help:"A different flag."`
|
||||
}
|
||||
var cli struct {
|
||||
Anonymous `envprefix:"ANON_"`
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
|
||||
_, err := p.Parse([]string{"--help"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, w.String(), "A flag ($ANON_FLAG1, $ANON_FLAG2).")
|
||||
assert.Contains(t, w.String(), "A different flag.")
|
||||
}
|
||||
|
||||
//nolint:dupl // false positive
|
||||
func TestCustomValueFormatter(t *testing.T) {
|
||||
var cli struct {
|
||||
Flag string `env:"FLAG" help:"A flag."`
|
||||
@@ -505,6 +535,24 @@ func TestCustomValueFormatter(t *testing.T) {
|
||||
assert.Contains(t, w.String(), "A flag.")
|
||||
}
|
||||
|
||||
//nolint:dupl // false positive
|
||||
func TestMultipleCustomValueFormatter(t *testing.T) {
|
||||
var cli struct {
|
||||
Flag string `env:"FLAG1,FLAG2" help:"A flag."`
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
p := mustNew(t, &cli,
|
||||
kong.Writers(w, w),
|
||||
kong.Exit(func(int) {}),
|
||||
kong.ValueFormatter(func(value *kong.Value) string {
|
||||
return value.Help
|
||||
}),
|
||||
)
|
||||
_, err := p.Parse([]string{"--help"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, w.String(), "A flag.")
|
||||
}
|
||||
|
||||
func TestAutoGroup(t *testing.T) {
|
||||
var cli struct {
|
||||
GroupedAString string `help:"A string flag grouped in A."`
|
||||
|
||||
@@ -227,11 +227,16 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
|
||||
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
|
||||
}
|
||||
if value.Flag != nil {
|
||||
if value.Flag.Env, err = interpolate(value.Flag.Env, vars, nil); err != nil {
|
||||
return fmt.Errorf("env value for %s: %s", value.Summary(), err)
|
||||
for i, env := range value.Flag.Envs {
|
||||
if value.Flag.Envs[i], err = interpolate(env, vars, nil); err != nil {
|
||||
return fmt.Errorf("env value for %s: %s", value.Summary(), err)
|
||||
}
|
||||
}
|
||||
value.Tag.Envs = value.Flag.Envs
|
||||
updatedVars["env"] = ""
|
||||
if len(value.Flag.Envs) != 0 {
|
||||
updatedVars["env"] = value.Flag.Envs[0]
|
||||
}
|
||||
value.Tag.Env = value.Flag.Env
|
||||
updatedVars["env"] = value.Flag.Env
|
||||
}
|
||||
value.Help, err = interpolate(value.Help, vars, updatedVars)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -665,7 +665,7 @@ func TestInterpolationIntoModel(t *testing.T) {
|
||||
assert.Equal(t, map[string]bool{"a": true, "b": true, "c": true, "d": true}, flag.EnumMap())
|
||||
assert.Equal(t, []string{"a", "b", "c", "d"}, flag.EnumSlice())
|
||||
assert.Equal(t, "One of a,b", flag2.Help)
|
||||
assert.Equal(t, "SAVE_THE_QUEEN", flag3.Env)
|
||||
assert.Equal(t, []string{"SAVE_THE_QUEEN"}, flag3.Envs)
|
||||
assert.Equal(t, "God SAVE_THE_QUEEN", flag3.Help)
|
||||
}
|
||||
|
||||
|
||||
@@ -366,14 +366,17 @@ func (v *Value) ApplyDefault() error {
|
||||
// Does not include resolvers.
|
||||
func (v *Value) Reset() error {
|
||||
v.Target.Set(reflect.Zero(v.Target.Type()))
|
||||
if v.Tag.Env != "" {
|
||||
envar := os.Getenv(v.Tag.Env)
|
||||
if envar != "" {
|
||||
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s (from envar %s=%q)", err, v.Tag.Env, envar)
|
||||
if len(v.Tag.Envs) != 0 {
|
||||
for _, env := range v.Tag.Envs {
|
||||
envar := os.Getenv(env)
|
||||
// Parse the first non-empty ENV in the list
|
||||
if envar != "" {
|
||||
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s (from envar %s=%q)", err, env, envar)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.HasDefault {
|
||||
@@ -393,7 +396,7 @@ type Flag struct {
|
||||
Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
|
||||
Xor []string
|
||||
PlaceHolder string
|
||||
Env string
|
||||
Envs []string
|
||||
Short rune
|
||||
Hidden bool
|
||||
Negated bool
|
||||
|
||||
+6
-6
@@ -451,21 +451,21 @@ func siftStrings(ss []string, filter func(s string) bool) []string {
|
||||
// --some.value -> PREFIX_SOME_VALUE
|
||||
func DefaultEnvars(prefix string) Option {
|
||||
processFlag := func(flag *Flag) {
|
||||
switch env := flag.Env; {
|
||||
switch env := flag.Envs; {
|
||||
case flag.Name == "help":
|
||||
return
|
||||
case env == "-":
|
||||
flag.Env = ""
|
||||
case len(env) == 1 && env[0] == "-":
|
||||
flag.Envs = nil
|
||||
return
|
||||
case env != "":
|
||||
case len(env) > 0:
|
||||
return
|
||||
}
|
||||
replacer := strings.NewReplacer("-", "_", ".", "_")
|
||||
names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
|
||||
names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
|
||||
name := strings.ToUpper(strings.Join(names, "_"))
|
||||
flag.Env = name
|
||||
flag.Value.Tag.Env = name
|
||||
flag.Envs = append(flag.Envs, name)
|
||||
flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name)
|
||||
}
|
||||
|
||||
var processNode func(node *Node)
|
||||
|
||||
@@ -58,6 +58,26 @@ func TestEnvarsFlagBasic(t *testing.T) {
|
||||
assert.Equal(t, "foo", cli.Interp)
|
||||
}
|
||||
|
||||
func TestEnvarsFlagMultiple(t *testing.T) {
|
||||
var cli struct {
|
||||
FirstENVPresent string `env:"KONG_TEST1_1,KONG_TEST1_2"`
|
||||
SecondENVPresent string `env:"KONG_TEST2_1,KONG_TEST2_2"`
|
||||
}
|
||||
parser, unsetEnvs := newEnvParser(t, &cli,
|
||||
envMap{
|
||||
"KONG_TEST1_1": "value1.1",
|
||||
"KONG_TEST1_2": "value1.2",
|
||||
"KONG_TEST2_2": "value2.2",
|
||||
},
|
||||
)
|
||||
defer unsetEnvs()
|
||||
|
||||
_, err := parser.Parse([]string{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value1.1", cli.FirstENVPresent)
|
||||
assert.Equal(t, "value2.2", cli.SecondENVPresent)
|
||||
}
|
||||
|
||||
func TestEnvarsFlagOverride(t *testing.T) {
|
||||
var cli struct {
|
||||
Flag string `env:"KONG_FLAG"`
|
||||
@@ -97,6 +117,23 @@ func TestEnvarsEnvPrefix(t *testing.T) {
|
||||
assert.Equal(t, []int{1, 2, 3}, cli.Slice)
|
||||
}
|
||||
|
||||
func TestEnvarsEnvPrefixMultiple(t *testing.T) {
|
||||
type Anonymous struct {
|
||||
Slice1 []int `env:"NUMBERS1_1,NUMBERS1_2"`
|
||||
Slice2 []int `env:"NUMBERS2_1,NUMBERS2_2"`
|
||||
}
|
||||
var cli struct {
|
||||
Anonymous `envprefix:"KONG_"`
|
||||
}
|
||||
parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"})
|
||||
defer restoreEnv()
|
||||
|
||||
_, err := parser.Parse([]string{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, cli.Slice1)
|
||||
assert.Equal(t, []int{5, 6, 7}, cli.Slice2)
|
||||
}
|
||||
|
||||
func TestEnvarsNestedEnvPrefix(t *testing.T) {
|
||||
type NestedAnonymous struct {
|
||||
String string `env:"STRING"`
|
||||
|
||||
@@ -24,7 +24,7 @@ type Tag struct {
|
||||
Default string
|
||||
Format string
|
||||
PlaceHolder string
|
||||
Env string
|
||||
Envs []string
|
||||
Short rune
|
||||
Hidden bool
|
||||
Sep rune
|
||||
@@ -234,7 +234,9 @@ func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
|
||||
t.Help = t.Get("help")
|
||||
t.Type = t.Get("type")
|
||||
t.TypeName = typeName
|
||||
t.Env = t.Get("env")
|
||||
for _, env := range t.GetAll("env") {
|
||||
t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...)
|
||||
}
|
||||
t.Short, err = t.GetRune("short")
|
||||
if err != nil && t.Get("short") != "" {
|
||||
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
|
||||
|
||||
Reference in New Issue
Block a user