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