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.
|
Both can coexist with standard Tag parsing.
|
||||||
|
|
||||||
| Tag | Description |
|
| Tag | Description |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `cmd:""` | If present, struct is a command. |
|
| `cmd:""` | If present, struct is a command. |
|
||||||
| `arg:""` | If present, field is an argument. Required by default. |
|
| `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. |
|
| `name:"X"` | Long name, for overriding field name. |
|
||||||
| `help:"X"` | Help text. |
|
| `help:"X"` | Help text. |
|
||||||
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
|
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
|
||||||
|
|||||||
@@ -138,8 +138,10 @@ MAIN:
|
|||||||
name = tag.Prefix + name
|
name = tag.Prefix + name
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.Env != "" {
|
if len(tag.Envs) != 0 {
|
||||||
tag.Env = tag.EnvPrefix + tag.Env
|
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.
|
// 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,
|
Value: value,
|
||||||
Short: tag.Short,
|
Short: tag.Short,
|
||||||
PlaceHolder: tag.PlaceHolder,
|
PlaceHolder: tag.PlaceHolder,
|
||||||
Env: tag.Env,
|
Envs: tag.Envs,
|
||||||
Group: buildGroupForKey(k, tag.Group),
|
Group: buildGroupForKey(k, tag.Group),
|
||||||
Xor: tag.Xor,
|
Xor: tag.Xor,
|
||||||
Hidden: tag.Hidden,
|
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 {
|
err := Visit(c.Model, func(node Visitable, next Next) error {
|
||||||
switch node := node.(type) {
|
switch node := node.(type) {
|
||||||
case *Value:
|
case *Value:
|
||||||
_, ok := os.LookupEnv(node.Tag.Env)
|
ok := atLeastOneEnvSet(node.Tag.Envs)
|
||||||
if node.Enum != "" && (!node.Required || node.HasDefault || (node.Tag.Env != "" && ok)) {
|
if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) {
|
||||||
if err := checkEnum(node, node.Target); err != nil {
|
if err := checkEnum(node, node.Target); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case *Flag:
|
case *Flag:
|
||||||
_, ok := os.LookupEnv(node.Tag.Env)
|
ok := atLeastOneEnvSet(node.Tag.Envs)
|
||||||
if node.Enum != "" && (!node.Required || node.HasDefault || (node.Tag.Env != "" && ok)) {
|
if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) {
|
||||||
if err := checkEnum(node.Value, node.Target); err != nil {
|
if err := checkEnum(node.Value, node.Target); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -890,9 +890,8 @@ func checkMissingPositionals(positional int, values []*Value) error {
|
|||||||
for ; positional < len(values); positional++ {
|
for ; positional < len(values); positional++ {
|
||||||
arg := values[positional]
|
arg := values[positional]
|
||||||
// TODO(aat): Fix hardcoding of these env checks all over the place :\
|
// TODO(aat): Fix hardcoding of these env checks all over the place :\
|
||||||
if arg.Tag.Env != "" {
|
if len(arg.Tag.Envs) != 0 {
|
||||||
_, ok := os.LookupEnv(arg.Tag.Env)
|
if atLeastOneEnvSet(arg.Tag.Envs) {
|
||||||
if ok {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -997,3 +996,12 @@ func isValidatable(v reflect.Value) validatable {
|
|||||||
}
|
}
|
||||||
return nil
|
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.
|
// DefaultHelpValueFormatter is the default HelpValueFormatter.
|
||||||
func DefaultHelpValueFormatter(value *Value) string {
|
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
|
return value.Help
|
||||||
}
|
}
|
||||||
suffix := "($" + value.Tag.Env + ")"
|
suffix := "(" + formatEnvs(value.Tag.Envs) + ")"
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(value.Help, "."):
|
case strings.HasSuffix(value.Help, "."):
|
||||||
return value.Help[:len(value.Help)-1] + " " + suffix + "."
|
return value.Help[:len(value.Help)-1] + " " + suffix + "."
|
||||||
@@ -567,3 +567,12 @@ func TreeIndenter(prefix string) string {
|
|||||||
}
|
}
|
||||||
return "|" + strings.Repeat(" ", defaultIndent) + prefix
|
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).")
|
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) {
|
func TestEnvarAutoHelpWithEnvPrefix(t *testing.T) {
|
||||||
type Anonymous struct {
|
type Anonymous struct {
|
||||||
Flag string `env:"FLAG" help:"A flag."`
|
Flag string `env:"FLAG" help:"A flag."`
|
||||||
@@ -488,6 +500,24 @@ func TestEnvarAutoHelpWithEnvPrefix(t *testing.T) {
|
|||||||
assert.Contains(t, w.String(), "A different flag.")
|
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) {
|
func TestCustomValueFormatter(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Flag string `env:"FLAG" help:"A flag."`
|
Flag string `env:"FLAG" help:"A flag."`
|
||||||
@@ -505,6 +535,24 @@ func TestCustomValueFormatter(t *testing.T) {
|
|||||||
assert.Contains(t, w.String(), "A flag.")
|
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) {
|
func TestAutoGroup(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
GroupedAString string `help:"A string flag grouped in A."`
|
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)
|
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
|
||||||
}
|
}
|
||||||
if value.Flag != nil {
|
if value.Flag != nil {
|
||||||
if value.Flag.Env, err = interpolate(value.Flag.Env, vars, nil); err != nil {
|
for i, env := range value.Flag.Envs {
|
||||||
return fmt.Errorf("env value for %s: %s", value.Summary(), err)
|
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)
|
value.Help, err = interpolate(value.Help, vars, updatedVars)
|
||||||
if err != nil {
|
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, 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, []string{"a", "b", "c", "d"}, flag.EnumSlice())
|
||||||
assert.Equal(t, "One of a,b", flag2.Help)
|
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)
|
assert.Equal(t, "God SAVE_THE_QUEEN", flag3.Help)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,14 +366,17 @@ func (v *Value) ApplyDefault() error {
|
|||||||
// Does not include resolvers.
|
// Does not include resolvers.
|
||||||
func (v *Value) Reset() error {
|
func (v *Value) Reset() error {
|
||||||
v.Target.Set(reflect.Zero(v.Target.Type()))
|
v.Target.Set(reflect.Zero(v.Target.Type()))
|
||||||
if v.Tag.Env != "" {
|
if len(v.Tag.Envs) != 0 {
|
||||||
envar := os.Getenv(v.Tag.Env)
|
for _, env := range v.Tag.Envs {
|
||||||
if envar != "" {
|
envar := os.Getenv(env)
|
||||||
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
|
// Parse the first non-empty ENV in the list
|
||||||
if err != nil {
|
if envar != "" {
|
||||||
return fmt.Errorf("%s (from envar %s=%q)", err, v.Tag.Env, 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 {
|
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.
|
Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
|
||||||
Xor []string
|
Xor []string
|
||||||
PlaceHolder string
|
PlaceHolder string
|
||||||
Env string
|
Envs []string
|
||||||
Short rune
|
Short rune
|
||||||
Hidden bool
|
Hidden bool
|
||||||
Negated bool
|
Negated bool
|
||||||
|
|||||||
+6
-6
@@ -451,21 +451,21 @@ func siftStrings(ss []string, filter func(s string) bool) []string {
|
|||||||
// --some.value -> PREFIX_SOME_VALUE
|
// --some.value -> PREFIX_SOME_VALUE
|
||||||
func DefaultEnvars(prefix string) Option {
|
func DefaultEnvars(prefix string) Option {
|
||||||
processFlag := func(flag *Flag) {
|
processFlag := func(flag *Flag) {
|
||||||
switch env := flag.Env; {
|
switch env := flag.Envs; {
|
||||||
case flag.Name == "help":
|
case flag.Name == "help":
|
||||||
return
|
return
|
||||||
case env == "-":
|
case len(env) == 1 && env[0] == "-":
|
||||||
flag.Env = ""
|
flag.Envs = nil
|
||||||
return
|
return
|
||||||
case env != "":
|
case len(env) > 0:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
replacer := strings.NewReplacer("-", "_", ".", "_")
|
replacer := strings.NewReplacer("-", "_", ".", "_")
|
||||||
names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
|
names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
|
||||||
names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
|
names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
|
||||||
name := strings.ToUpper(strings.Join(names, "_"))
|
name := strings.ToUpper(strings.Join(names, "_"))
|
||||||
flag.Env = name
|
flag.Envs = append(flag.Envs, name)
|
||||||
flag.Value.Tag.Env = name
|
flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var processNode func(node *Node)
|
var processNode func(node *Node)
|
||||||
|
|||||||
@@ -58,6 +58,26 @@ func TestEnvarsFlagBasic(t *testing.T) {
|
|||||||
assert.Equal(t, "foo", cli.Interp)
|
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) {
|
func TestEnvarsFlagOverride(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Flag string `env:"KONG_FLAG"`
|
Flag string `env:"KONG_FLAG"`
|
||||||
@@ -97,6 +117,23 @@ func TestEnvarsEnvPrefix(t *testing.T) {
|
|||||||
assert.Equal(t, []int{1, 2, 3}, cli.Slice)
|
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) {
|
func TestEnvarsNestedEnvPrefix(t *testing.T) {
|
||||||
type NestedAnonymous struct {
|
type NestedAnonymous struct {
|
||||||
String string `env:"STRING"`
|
String string `env:"STRING"`
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Tag struct {
|
|||||||
Default string
|
Default string
|
||||||
Format string
|
Format string
|
||||||
PlaceHolder string
|
PlaceHolder string
|
||||||
Env string
|
Envs []string
|
||||||
Short rune
|
Short rune
|
||||||
Hidden bool
|
Hidden bool
|
||||||
Sep rune
|
Sep rune
|
||||||
@@ -234,7 +234,9 @@ func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
|
|||||||
t.Help = t.Get("help")
|
t.Help = t.Get("help")
|
||||||
t.Type = t.Get("type")
|
t.Type = t.Get("type")
|
||||||
t.TypeName = typeName
|
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")
|
t.Short, err = t.GetRune("short")
|
||||||
if err != nil && t.Get("short") != "" {
|
if err != nil && t.Get("short") != "" {
|
||||||
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
|
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
|
||||||
|
|||||||
Reference in New Issue
Block a user