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. [Custom named types](#custom-named-types)
1. [Supported tags](#supported-tags)
1. [Variable interpolation](#variable-interpolation)
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. [`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 |
|-------------------|---------------------------------------------------|
| `file` | A path. ~ expansion is applied. |
| `existingfile` | An existing path. ~ expansion is applied. |
| `path` | A path. ~ expansion is applied. |
| `existingfile` | An existing file. ~ 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. |
| `format:"X"` | Format for parsing input, if supported. |
| `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
+1
View File
@@ -151,6 +151,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
Mapper: mapper,
Tag: tag,
Target: fv,
Enum: tag.Enum,
// Flags are optional by default, and args are required by default.
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
helpOptions HelpOptions
helpFlag *Flag
vars map[string]string
// Set temporarily by Options. These are applied after build().
postBuildOptions []Option
@@ -67,6 +68,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
before: map[reflect.Value]HookFunc{},
registry: NewRegistry().RegisterDefaults(),
resolvers: []ResolverFunc{Envars()},
vars: map[string]string{},
}
for _, option := range options {
@@ -88,15 +90,71 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.Model.HelpFlag = k.helpFlag
for _, option := range k.postBuildOptions {
if err := option(k); err != nil {
if err = option(k); err != nil {
return nil, err
}
}
k.postBuildOptions = nil
if err = k.interpolate(k.Model.Node); err != nil {
return nil, err
}
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.
func (k *Kong) extraFlags() []*Flag {
if k.noDefaultHelp {
+21
View File
@@ -512,3 +512,24 @@ func TestRun(t *testing.T) {
err = ctx.Run("ERROR")
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.
type Value struct {
Flag *Flag
Flag *Flag // Nil if positional argument.
Name string
Help string
Default string
Enum string
Mapper Mapper
Tag *Tag
Target reflect.Value
@@ -198,6 +199,16 @@ type Value struct {
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.
func (v *Value) Summary() string {
if v.Flag != nil {
+10
View File
@@ -12,6 +12,16 @@ import (
// An Option applies optional changes to the Kong application.
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.
func Exit(exit func(int)) Option {
return func(k *Kong) error {
+2 -6
View File
@@ -24,7 +24,7 @@ type Tag struct {
Short rune
Hidden bool
Sep rune
Enum map[string]bool
Enum string
// Storage for all tag keys for arbitrary lookups.
items map[string]string
@@ -114,7 +114,6 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
s, chars := getTagInfo(ft)
t := &Tag{
items: parseTagItems(s, chars),
Enum: map[string]bool{},
}
t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg")
@@ -149,10 +148,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
}
for _, part := range strings.Split(t.Get("enum"), ",") {
t.Enum[part] = true
}
t.Enum = t.Get("enum")
return t
}