Support limited variable interpolation.

Kong supports limited variable interpolation into help strings, enum lists and
default values.

Variables are in the form:

    ${<name>}

Variables are set with the `Vars(map[string]string)` option. Undefined
variable references in the grammar will result in an error at construction
time.
This commit is contained in:
Alec Thomas
2018-06-27 21:07:06 +10:00
parent 6408010083
commit 1bb0c0b4b2
9 changed files with 187 additions and 10 deletions
+38 -2
View File
@@ -18,6 +18,7 @@
1. [Maps](#maps) 1. [Maps](#maps)
1. [Custom named types](#custom-named-types) 1. [Custom named types](#custom-named-types)
1. [Supported tags](#supported-tags) 1. [Supported tags](#supported-tags)
1. [Variable interpolation](#variable-interpolation)
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour) 1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) 1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) 1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
@@ -313,8 +314,8 @@ function `NamedMapper(name, mapper)`.
| Name | Description | | Name | Description |
|-------------------|---------------------------------------------------| |-------------------|---------------------------------------------------|
| `file` | A path. ~ expansion is applied. | | `path` | A path. ~ expansion is applied. |
| `existingfile` | An existing path. ~ expansion is applied. | | `existingfile` | An existing file. ~ expansion is applied. |
| `existingdir` | An existing directory. ~ expansion is applied. | | `existingdir` | An existing directory. ~ expansion is applied. |
@@ -348,6 +349,41 @@ Both can coexist with standard Tag parsing.
| `hidden` | If present, command or flag is hidden. | | `hidden` | If present, command or flag is hidden. |
| `format:"X"` | Format for parsing input, if supported. | | `format:"X"` | Format for parsing input, if supported. |
| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. | | `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
| `enum:"X,Y,..."` |
## Variable interpolation
Kong supports limited variable interpolation into help strings, enum lists and
default values.
Variables are in the form:
${<name>}
Variables are set with the `Vars(map[string]string)` option. Undefined
variable references in the grammar will result in an error at construction
time.
When interpolating into flag or argument help strings, some extra variables
are defined from the value itself:
${default}
${enum}
eg.
```go
type cli struct {
Config string `type:"path" default:"${config_file}"`
}
func main() {
kong.Parse(&cli,
kong.Vars(map[string]string{
"config_file": "~/.app.conf",
}))
}
```
## Modifying Kong's behaviour ## Modifying Kong's behaviour
+1
View File
@@ -151,6 +151,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
Mapper: mapper, Mapper: mapper,
Tag: tag, Tag: tag,
Target: fv, Target: fv,
Enum: tag.Enum,
// Flags are optional by default, and args are required by default. // Flags are optional by default, and args are required by default.
Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional), Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
+27
View File
@@ -0,0 +1,27 @@
package kong
import (
"fmt"
"regexp"
)
var interpolationRegex = regexp.MustCompile(`(\${[[:alpha:]_][[:word:]]*})|(\$)|([^$]+)`)
// Interpolate variables from vars into s for substrings in the form ${var}.
func interpolate(s string, vars map[string]string) (string, error) {
out := ""
matches := interpolationRegex.FindAllStringSubmatch(s, -1)
for _, match := range matches {
if match[1] != "" {
name := match[1][2 : len(match[1])-1]
value, ok := vars[name]
if !ok {
return "", fmt.Errorf("undefined variable ${%s}", name)
}
out += value
} else {
out += match[0]
}
}
return out, nil
}
+17
View File
@@ -0,0 +1,17 @@
package kong
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestInterpolate(t *testing.T) {
vars := map[string]string{
"name": "Bobby Brown",
"age": "35",
}
actual, err := interpolate("${name} is ${age} years old", vars)
require.NoError(t, err)
require.Equal(t, `Bobby Brown is 35 years old`, actual)
}
+59 -1
View File
@@ -51,6 +51,7 @@ type Kong struct {
help HelpPrinter help HelpPrinter
helpOptions HelpOptions helpOptions HelpOptions
helpFlag *Flag helpFlag *Flag
vars map[string]string
// Set temporarily by Options. These are applied after build(). // Set temporarily by Options. These are applied after build().
postBuildOptions []Option postBuildOptions []Option
@@ -67,6 +68,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
before: map[reflect.Value]HookFunc{}, before: map[reflect.Value]HookFunc{},
registry: NewRegistry().RegisterDefaults(), registry: NewRegistry().RegisterDefaults(),
resolvers: []ResolverFunc{Envars()}, resolvers: []ResolverFunc{Envars()},
vars: map[string]string{},
} }
for _, option := range options { for _, option := range options {
@@ -88,15 +90,71 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.Model.HelpFlag = k.helpFlag k.Model.HelpFlag = k.helpFlag
for _, option := range k.postBuildOptions { for _, option := range k.postBuildOptions {
if err := option(k); err != nil { if err = option(k); err != nil {
return nil, err return nil, err
} }
} }
k.postBuildOptions = nil k.postBuildOptions = nil
if err = k.interpolate(k.Model.Node); err != nil {
return nil, err
}
return k, nil return k, nil
} }
// Interpolate variables into model.
func (k *Kong) interpolate(node *Node) (err error) {
node.Help, err = interpolate(node.Help, k.vars)
if err != nil {
return fmt.Errorf("help for %s: %s", node.Path(), err)
}
for _, flag := range node.Flags {
if err = k.interpolateValue(flag.Value); err != nil {
return err
}
}
for _, pos := range node.Positional {
if err = k.interpolateValue(pos); err != nil {
return err
}
}
for _, child := range node.Children {
if err = k.interpolate(child); err != nil {
return err
}
}
return nil
}
func (k *Kong) interpolateValue(value *Value) (err error) {
if value.Default, err = interpolate(value.Default, k.vars); err != nil {
return fmt.Errorf("default value for %s: %s", value.Summary(), err)
}
if value.Enum, err = interpolate(value.Enum, k.vars); err != nil {
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
}
vars := mergeVars(k.vars, map[string]string{
"default": value.Default,
"enum": value.Enum,
})
if value.Help, err = interpolate(value.Help, vars); err != nil {
return fmt.Errorf("help for %s: %s", value.Summary(), err)
}
return nil
}
func mergeVars(base, extra map[string]string) map[string]string {
out := make(map[string]string, len(base)+len(extra))
for k, v := range base {
out[k] = v
}
for k, v := range extra {
out[k] = v
}
return out
}
// Provide additional builtin flags, if any. // Provide additional builtin flags, if any.
func (k *Kong) extraFlags() []*Flag { func (k *Kong) extraFlags() []*Flag {
if k.noDefaultHelp { if k.noDefaultHelp {
+21
View File
@@ -512,3 +512,24 @@ func TestRun(t *testing.T) {
err = ctx.Run("ERROR") err = ctx.Run("ERROR")
require.Error(t, err) require.Error(t, err)
} }
func TestInterpolationIntoModel(t *testing.T) {
var cli struct {
Flag string `default:"${default}" help:"Help, I need ${somebody}" enum:"${enum}"`
EnumRef string `enum:"a,b" help:"One of ${enum}"`
}
_, err := kong.New(&cli)
require.Error(t, err)
p, err := kong.New(&cli, kong.Vars(map[string]string{
"default": "Some default value.",
"somebody": "chickens!",
"enum": "a,b,c,d",
}))
require.NoError(t, err)
flag := p.Model.Flags[1]
flag2 := p.Model.Flags[2]
require.Equal(t, "Some default value.", flag.Default)
require.Equal(t, "Help, I need chickens!", flag.Help)
require.Equal(t, map[string]bool{"a": true, "b": true, "c": true, "d": true}, flag.EnumMap())
require.Equal(t, "One of a,b", flag2.Help)
}
+12 -1
View File
@@ -185,10 +185,11 @@ func (n *Node) Path() (out string) {
// A Value is either a flag or a variable positional argument. // A Value is either a flag or a variable positional argument.
type Value struct { type Value struct {
Flag *Flag Flag *Flag // Nil if positional argument.
Name string Name string
Help string Help string
Default string Default string
Enum string
Mapper Mapper Mapper Mapper
Tag *Tag Tag *Tag
Target reflect.Value Target reflect.Value
@@ -198,6 +199,16 @@ type Value struct {
Position int // Position (for positional arguments). Position int // Position (for positional arguments).
} }
// EnumMap returns a map of the enums in this value.
func (v *Value) EnumMap() map[string]bool {
parts := strings.Split(v.Enum, ",")
out := make(map[string]bool, len(parts))
for _, part := range parts {
out[strings.TrimSpace(part)] = true
}
return out
}
// Summary returns a human-readable summary of the value. // Summary returns a human-readable summary of the value.
func (v *Value) Summary() string { func (v *Value) Summary() string {
if v.Flag != nil { if v.Flag != nil {
+10
View File
@@ -12,6 +12,16 @@ import (
// An Option applies optional changes to the Kong application. // An Option applies optional changes to the Kong application.
type Option func(k *Kong) error type Option func(k *Kong) error
// Vars sets the variables to use for interpolation into help strings and default values.
//
// See README for details.
func Vars(vars map[string]string) Option {
return func(k *Kong) error {
k.vars = vars
return nil
}
}
// Exit overrides the function used to terminate. This is useful for testing or interactive use. // Exit overrides the function used to terminate. This is useful for testing or interactive use.
func Exit(exit func(int)) Option { func Exit(exit func(int)) Option {
return func(k *Kong) error { return func(k *Kong) error {
+2 -6
View File
@@ -24,7 +24,7 @@ type Tag struct {
Short rune Short rune
Hidden bool Hidden bool
Sep rune Sep rune
Enum map[string]bool Enum string
// Storage for all tag keys for arbitrary lookups. // Storage for all tag keys for arbitrary lookups.
items map[string]string items map[string]string
@@ -114,7 +114,6 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
s, chars := getTagInfo(ft) s, chars := getTagInfo(ft)
t := &Tag{ t := &Tag{
items: parseTagItems(s, chars), items: parseTagItems(s, chars),
Enum: map[string]bool{},
} }
t.Cmd = t.Has("cmd") t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg") t.Arg = t.Has("arg")
@@ -149,10 +148,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
if t.PlaceHolder == "" { if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
} }
for _, part := range strings.Split(t.Get("enum"), ",") { t.Enum = t.Get("enum")
t.Enum[part] = true
}
return t return t
} }