Add support for setting variables via tag.
This provides much more convenient composition when reusing structs in
different parts of the command grammar.
eg.
type Embedded struct {
Key string `help:"A key from ${where}."`
}
var cli struct {
Embedded `set:"where=somewhere"`
}
This commit is contained in:
@@ -394,25 +394,26 @@ 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. |
|
`arg` | If present, field is an argument.
|
||||||
| `env:"X"` | Specify envar to use for default value.
|
`env:"X"` | Specify envar to use for default value.
|
||||||
| `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-types) to use. |
|
`type:"X"` | Specify [named types](#custom-named-types) to use.
|
||||||
| `placeholder:"X"` | Placeholder text. |
|
`placeholder:"X"` | Placeholder text.
|
||||||
| `default:"X"` | Default value. |
|
`default:"X"` | Default value.
|
||||||
| `short:"X"` | Short name, if flag. |
|
`short:"X"` | Short name, if flag.
|
||||||
| `required` | If present, flag/arg is required. |
|
`required` | If present, flag/arg is required.
|
||||||
| `optional` | If present, flag/arg is optional. |
|
`optional` | If present, flag/arg is optional.
|
||||||
| `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,..."` | Set of valid values allowed for this flag. |
|
`enum:"X,Y,..."` | Set of valid values allowed for this flag.
|
||||||
| `group:"X"` | Logical group for a flag or command. |
|
`group:"X"` | Logical group for a flag or command.
|
||||||
| `prefix:"X"` | Prefix for all sub-flags. |
|
`prefix:"X"` | Prefix for all sub-flags.
|
||||||
|
`set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur.
|
||||||
|
|
||||||
<a id="markdown-variable-interpolation" name="variable-interpolation"></a>
|
<a id="markdown-variable-interpolation" name="variable-interpolation"></a>
|
||||||
## Variable interpolation
|
## Variable interpolation
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
|
|||||||
}
|
}
|
||||||
app.Node = node
|
app.Node = node
|
||||||
app.Node.Flags = append(extraFlags, app.Node.Flags...)
|
app.Node.Flags = append(extraFlags, app.Node.Flags...)
|
||||||
|
app.Tag = newEmptyTag()
|
||||||
|
app.Tag.Vars = k.vars
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +64,8 @@ func flattenedFields(v reflect.Value) (out []flattenedField) {
|
|||||||
}
|
}
|
||||||
// Accumulate prefixes.
|
// Accumulate prefixes.
|
||||||
subf.tag.Prefix = tag.Prefix + subf.tag.Prefix
|
subf.tag.Prefix = tag.Prefix + subf.tag.Prefix
|
||||||
|
// Combine parent vars.
|
||||||
|
subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars)
|
||||||
}
|
}
|
||||||
out = append(out, sub...)
|
out = append(out, sub...)
|
||||||
continue
|
continue
|
||||||
@@ -78,6 +82,7 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool
|
|||||||
node := &Node{
|
node := &Node{
|
||||||
Type: typ,
|
Type: typ,
|
||||||
Target: v,
|
Target: v,
|
||||||
|
Tag: newEmptyTag(),
|
||||||
}
|
}
|
||||||
for _, field := range flattenedFields(v) {
|
for _, field := range flattenedFields(v) {
|
||||||
ft := field.field
|
ft := field.field
|
||||||
@@ -124,6 +129,7 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool
|
|||||||
|
|
||||||
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
|
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
|
||||||
child := buildNode(k, fv, typ, seenFlags)
|
child := buildNode(k, fv, typ, seenFlags)
|
||||||
|
child.Tag = tag
|
||||||
child.Parent = node
|
child.Parent = node
|
||||||
child.Help = tag.Help
|
child.Help = tag.Help
|
||||||
child.Hidden = tag.Hidden
|
child.Hidden = tag.Hidden
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module github.com/alecthomas/kong
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.2.2
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
@@ -110,17 +110,18 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
|
|||||||
|
|
||||||
// Interpolate variables into model.
|
// Interpolate variables into model.
|
||||||
func (k *Kong) interpolate(node *Node) (err error) {
|
func (k *Kong) interpolate(node *Node) (err error) {
|
||||||
node.Help, err = interpolate(node.Help, k.vars)
|
vars := node.Vars()
|
||||||
|
node.Help, err = interpolate(node.Help, vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("help for %s: %s", node.Path(), err)
|
return fmt.Errorf("help for %s: %s", node.Path(), err)
|
||||||
}
|
}
|
||||||
for _, flag := range node.Flags {
|
for _, flag := range node.Flags {
|
||||||
if err = k.interpolateValue(flag.Value); err != nil {
|
if err = k.interpolateValue(flag.Value, vars); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, pos := range node.Positional {
|
for _, pos := range node.Positional {
|
||||||
if err = k.interpolateValue(pos); err != nil {
|
if err = k.interpolateValue(pos, vars); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,14 +133,15 @@ func (k *Kong) interpolate(node *Node) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *Kong) interpolateValue(value *Value) (err error) {
|
func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
|
||||||
if value.Default, err = interpolate(value.Default, k.vars); err != nil {
|
vars = vars.CloneWith(value.Tag.Vars)
|
||||||
|
if value.Default, err = interpolate(value.Default, vars); err != nil {
|
||||||
return fmt.Errorf("default value for %s: %s", value.Summary(), err)
|
return fmt.Errorf("default value for %s: %s", value.Summary(), err)
|
||||||
}
|
}
|
||||||
if value.Enum, err = interpolate(value.Enum, k.vars); err != nil {
|
if value.Enum, err = interpolate(value.Enum, vars); err != nil {
|
||||||
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
|
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
|
||||||
}
|
}
|
||||||
vars := mergeVars(k.vars, map[string]string{
|
vars = vars.CloneWith(map[string]string{
|
||||||
"default": value.Default,
|
"default": value.Default,
|
||||||
"enum": value.Enum,
|
"enum": value.Enum,
|
||||||
})
|
})
|
||||||
@@ -149,17 +151,6 @@ func (k *Kong) interpolateValue(value *Value) (err error) {
|
|||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type helpValue bool
|
type helpValue bool
|
||||||
|
|
||||||
func (h helpValue) BeforeApply(ctx *Context) error {
|
func (h helpValue) BeforeApply(ctx *Context) error {
|
||||||
@@ -252,7 +243,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
|
|||||||
if !method.IsValid() {
|
if !method.IsValid() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
binds := k.bindings.clone().add(ctx, trace)
|
binds := k.bindings.clone().add(ctx, trace).add(trace.Node().Vars().CloneWith(k.vars))
|
||||||
if err := callMethod(name, value, method, binds); err != nil {
|
if err := callMethod(name, value, method, binds); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Node struct {
|
|||||||
Positional []*Positional
|
Positional []*Positional
|
||||||
Children []*Node
|
Children []*Node
|
||||||
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
|
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
|
||||||
|
Tag *Tag
|
||||||
|
|
||||||
Argument *Value // Populated when Type is ArgumentNode.
|
Argument *Value // Populated when Type is ArgumentNode.
|
||||||
}
|
}
|
||||||
@@ -171,6 +172,14 @@ func (n *Node) FullPath() string {
|
|||||||
return strings.TrimSpace(root.Name + " " + n.Path())
|
return strings.TrimSpace(root.Name + " " + n.Path())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vars returns the combined Vars defined by all ancestors of this Node.
|
||||||
|
func (n *Node) Vars() Vars {
|
||||||
|
if n == nil {
|
||||||
|
return Vars{}
|
||||||
|
}
|
||||||
|
return n.Parent.Vars().CloneWith(n.Tag.Vars)
|
||||||
|
}
|
||||||
|
|
||||||
// Path through ancestors to this Node.
|
// Path through ancestors to this Node.
|
||||||
func (n *Node) Path() (out string) {
|
func (n *Node) Path() (out string) {
|
||||||
if n.Parent != nil {
|
if n.Parent != nil {
|
||||||
|
|||||||
+12
@@ -29,6 +29,18 @@ func (v Vars) Apply(k *Kong) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloneWith clones the current Vars and merges "vars" onto the clone.
|
||||||
|
func (v Vars) CloneWith(vars Vars) Vars {
|
||||||
|
out := Vars{}
|
||||||
|
for key, value := range v {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
for key, value := range vars {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// 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)) OptionFunc {
|
func Exit(exit func(int)) OptionFunc {
|
||||||
return func(k *Kong) error {
|
return func(k *Kong) error {
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ type Tag struct {
|
|||||||
Sep rune
|
Sep rune
|
||||||
Enum string
|
Enum string
|
||||||
Group string
|
Group string
|
||||||
|
Vars Vars
|
||||||
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
|
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
|
||||||
|
|
||||||
// Storage for all tag keys for arbitrary lookups.
|
// Storage for all tag keys for arbitrary lookups.
|
||||||
items map[string]string
|
items map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type tagChars struct {
|
type tagChars struct {
|
||||||
@@ -40,15 +41,15 @@ type tagChars struct {
|
|||||||
var kongChars = tagChars{sep: ',', quote: '\'', assign: '='}
|
var kongChars = tagChars{sep: ',', quote: '\'', assign: '='}
|
||||||
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'}
|
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'}
|
||||||
|
|
||||||
func parseTagItems(tagString string, chr tagChars) map[string]string {
|
func parseTagItems(tagString string, chr tagChars) map[string][]string {
|
||||||
d := map[string]string{}
|
d := map[string][]string{}
|
||||||
key := []rune{}
|
key := []rune{}
|
||||||
value := []rune{}
|
value := []rune{}
|
||||||
quotes := false
|
quotes := false
|
||||||
inKey := true
|
inKey := true
|
||||||
|
|
||||||
add := func() {
|
add := func() {
|
||||||
d[string(key)] = string(value)
|
d[string(key)] = append(d[string(key)], string(value))
|
||||||
key = []rune{}
|
key = []rune{}
|
||||||
value = []rune{}
|
value = []rune{}
|
||||||
inKey = true
|
inKey = true
|
||||||
@@ -113,13 +114,18 @@ func getTagInfo(ft reflect.StructField) (string, tagChars) {
|
|||||||
return string(ft.Tag), bareChars
|
return string(ft.Tag), bareChars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newEmptyTag() *Tag {
|
||||||
|
return &Tag{items: map[string][]string{}}
|
||||||
|
}
|
||||||
|
|
||||||
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
||||||
if ft.Tag.Get("kong") == "-" {
|
if ft.Tag.Get("kong") == "-" {
|
||||||
return &Tag{Ignored: true, items: map[string]string{}}
|
t := newEmptyTag()
|
||||||
|
t.Ignored = true
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
s, chars := getTagInfo(ft)
|
|
||||||
t := &Tag{
|
t := &Tag{
|
||||||
items: parseTagItems(s, chars),
|
items: parseTagItems(getTagInfo(ft)),
|
||||||
}
|
}
|
||||||
t.Cmd = t.Has("cmd")
|
t.Cmd = t.Has("cmd")
|
||||||
t.Arg = t.Has("arg")
|
t.Arg = t.Has("arg")
|
||||||
@@ -152,6 +158,14 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
t.Sep = ','
|
t.Sep = ','
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.Vars = Vars{}
|
||||||
|
for _, set := range t.GetAll("set") {
|
||||||
|
parts := strings.SplitN(set, "=", 2)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
fail("set should be in the form key=value but got %q", set)
|
||||||
|
}
|
||||||
|
t.Vars[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
t.PlaceHolder = t.Get("placeholder")
|
t.PlaceHolder = t.Get("placeholder")
|
||||||
if t.PlaceHolder == "" {
|
if t.PlaceHolder == "" {
|
||||||
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
|
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
|
||||||
@@ -170,29 +184,38 @@ func (t *Tag) Has(k string) bool {
|
|||||||
//
|
//
|
||||||
// Note that this will return the empty string if the tag is missing.
|
// Note that this will return the empty string if the tag is missing.
|
||||||
func (t *Tag) Get(k string) string {
|
func (t *Tag) Get(k string) string {
|
||||||
|
values := t.items[k]
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll returns all encountered values for a tag, in the case of multiple occurrences.
|
||||||
|
func (t *Tag) GetAll(k string) []string {
|
||||||
return t.items[k]
|
return t.items[k]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBool returns true if the given tag looks like a boolean truth string.
|
// GetBool returns true if the given tag looks like a boolean truth string.
|
||||||
func (t *Tag) GetBool(k string) (bool, error) {
|
func (t *Tag) GetBool(k string) (bool, error) {
|
||||||
return strconv.ParseBool(t.items[k])
|
return strconv.ParseBool(t.Get(k))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFloat parses the given tag as a float64.
|
// GetFloat parses the given tag as a float64.
|
||||||
func (t *Tag) GetFloat(k string) (float64, error) {
|
func (t *Tag) GetFloat(k string) (float64, error) {
|
||||||
return strconv.ParseFloat(t.items[k], 64)
|
return strconv.ParseFloat(t.Get(k), 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt parses the given tag as an int64.
|
// GetInt parses the given tag as an int64.
|
||||||
func (t *Tag) GetInt(k string) (int64, error) {
|
func (t *Tag) GetInt(k string) (int64, error) {
|
||||||
return strconv.ParseInt(t.items[k], 10, 64)
|
return strconv.ParseInt(t.Get(k), 10, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRune parses the given tag as a rune.
|
// GetRune parses the given tag as a rune.
|
||||||
func (t *Tag) GetRune(k string) (rune, error) {
|
func (t *Tag) GetRune(k string) (rune, error) {
|
||||||
r, _ := utf8.DecodeRuneInString(t.items[k])
|
r, _ := utf8.DecodeRuneInString(t.Get(k))
|
||||||
if r == utf8.RuneError {
|
if r == utf8.RuneError {
|
||||||
return 0, fmt.Errorf("%v has a rune error", t.items[k])
|
return 0, fmt.Errorf("%v has a rune error", t.Get(k))
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -1,6 +1,7 @@
|
|||||||
package kong_test
|
package kong_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -109,3 +110,31 @@ func TestManySeps(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "hi", cli.Arg)
|
require.Equal(t, "hi", cli.Arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTagSetOnEmbeddedStruct(t *testing.T) {
|
||||||
|
type Embedded struct {
|
||||||
|
Key string `help:"A key from ${where}."`
|
||||||
|
}
|
||||||
|
var cli struct {
|
||||||
|
Embedded `set:"where=somewhere"`
|
||||||
|
}
|
||||||
|
buf := &strings.Builder{}
|
||||||
|
p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {}))
|
||||||
|
_, err := p.Parse([]string{"--help"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, buf.String(), `A key from somewhere.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagSetOnCommand(t *testing.T) {
|
||||||
|
type Command struct {
|
||||||
|
Key string `help:"A key from ${where}."`
|
||||||
|
}
|
||||||
|
var cli struct {
|
||||||
|
Command Command `set:"where=somewhere" cmd:""`
|
||||||
|
}
|
||||||
|
buf := &strings.Builder{}
|
||||||
|
p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {}))
|
||||||
|
_, err := p.Parse([]string{"command", "--help"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, buf.String(), `A key from somewhere.`)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user