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:
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user