diff --git a/README.md b/README.md index f11e50d..273691b 100644 --- a/README.md +++ b/README.md @@ -394,25 +394,26 @@ Tags can be in two forms: Both can coexist with standard Tag parsing. -| Tag | Description | -| -----------------------| ------------------------------------------- | -| `cmd` | If present, struct is a command. | -| `arg` | If present, field is an argument. | -| `env:"X"` | Specify envar to use for default value. -| `name:"X"` | Long name, for overriding field name. | -| `help:"X"` | Help text. | -| `type:"X"` | Specify [named types](#custom-named-types) to use. | -| `placeholder:"X"` | Placeholder text. | -| `default:"X"` | Default value. | -| `short:"X"` | Short name, if flag. | -| `required` | If present, flag/arg is required. | -| `optional` | If present, flag/arg is optional. | -| `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,..."` | Set of valid values allowed for this flag. | -| `group:"X"` | Logical group for a flag or command. | -| `prefix:"X"` | Prefix for all sub-flags. | +Tag | Description +-----------------------| ------------------------------------------- +`cmd` | If present, struct is a command. +`arg` | If present, field is an argument. +`env:"X"` | Specify envar to use for default value. +`name:"X"` | Long name, for overriding field name. +`help:"X"` | Help text. +`type:"X"` | Specify [named types](#custom-named-types) to use. +`placeholder:"X"` | Placeholder text. +`default:"X"` | Default value. +`short:"X"` | Short name, if flag. +`required` | If present, flag/arg is required. +`optional` | If present, flag/arg is optional. +`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,..."` | Set of valid values allowed for this flag. +`group:"X"` | Logical group for a flag or command. +`prefix:"X"` | Prefix for all sub-flags. +`set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. ## Variable interpolation diff --git a/build.go b/build.go index 4a487b9..ae1ccf7 100644 --- a/build.go +++ b/build.go @@ -28,6 +28,8 @@ func build(k *Kong, ast interface{}) (app *Application, err error) { } app.Node = node app.Node.Flags = append(extraFlags, app.Node.Flags...) + app.Tag = newEmptyTag() + app.Tag.Vars = k.vars return app, nil } @@ -62,6 +64,8 @@ func flattenedFields(v reflect.Value) (out []flattenedField) { } // Accumulate prefixes. subf.tag.Prefix = tag.Prefix + subf.tag.Prefix + // Combine parent vars. + subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars) } out = append(out, sub...) continue @@ -78,6 +82,7 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool node := &Node{ Type: typ, Target: v, + Tag: newEmptyTag(), } for _, field := range flattenedFields(v) { 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) { child := buildNode(k, fv, typ, seenFlags) + child.Tag = tag child.Parent = node child.Help = tag.Help child.Hidden = tag.Hidden diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da4aff7 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e03ee77 --- /dev/null +++ b/go.sum @@ -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= diff --git a/kong.go b/kong.go index fad3113..bce8f94 100644 --- a/kong.go +++ b/kong.go @@ -110,17 +110,18 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { // Interpolate variables into model. 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 { return fmt.Errorf("help for %s: %s", node.Path(), err) } for _, flag := range node.Flags { - if err = k.interpolateValue(flag.Value); err != nil { + if err = k.interpolateValue(flag.Value, vars); err != nil { return err } } for _, pos := range node.Positional { - if err = k.interpolateValue(pos); err != nil { + if err = k.interpolateValue(pos, vars); err != nil { return err } } @@ -132,14 +133,15 @@ func (k *Kong) interpolate(node *Node) (err error) { return nil } -func (k *Kong) interpolateValue(value *Value) (err error) { - if value.Default, err = interpolate(value.Default, k.vars); err != nil { +func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { + 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) } - 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) } - vars := mergeVars(k.vars, map[string]string{ + vars = vars.CloneWith(map[string]string{ "default": value.Default, "enum": value.Enum, }) @@ -149,17 +151,6 @@ func (k *Kong) interpolateValue(value *Value) (err error) { 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 func (h helpValue) BeforeApply(ctx *Context) error { @@ -252,7 +243,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error { if !method.IsValid() { 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 { return err } diff --git a/model.go b/model.go index db0fa58..8103bf5 100644 --- a/model.go +++ b/model.go @@ -43,6 +43,7 @@ type Node struct { Positional []*Positional Children []*Node 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. } @@ -171,6 +172,14 @@ func (n *Node) FullPath() string { 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. func (n *Node) Path() (out string) { if n.Parent != nil { diff --git a/options.go b/options.go index 1bf3218..5bcec7e 100644 --- a/options.go +++ b/options.go @@ -29,6 +29,18 @@ func (v Vars) Apply(k *Kong) error { 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. func Exit(exit func(int)) OptionFunc { return func(k *Kong) error { diff --git a/tag.go b/tag.go index 5ade2f1..7167a9e 100644 --- a/tag.go +++ b/tag.go @@ -27,10 +27,11 @@ type Tag struct { Sep rune Enum string Group string + Vars Vars Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. // Storage for all tag keys for arbitrary lookups. - items map[string]string + items map[string][]string } type tagChars struct { @@ -40,15 +41,15 @@ type tagChars struct { var kongChars = tagChars{sep: ',', quote: '\'', assign: '='} var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'} -func parseTagItems(tagString string, chr tagChars) map[string]string { - d := map[string]string{} +func parseTagItems(tagString string, chr tagChars) map[string][]string { + d := map[string][]string{} key := []rune{} value := []rune{} quotes := false inKey := true add := func() { - d[string(key)] = string(value) + d[string(key)] = append(d[string(key)], string(value)) key = []rune{} value = []rune{} inKey = true @@ -113,13 +114,18 @@ func getTagInfo(ft reflect.StructField) (string, tagChars) { return string(ft.Tag), bareChars } +func newEmptyTag() *Tag { + return &Tag{items: map[string][]string{}} +} + func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { 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{ - items: parseTagItems(s, chars), + items: parseTagItems(getTagInfo(ft)), } t.Cmd = t.Has("cmd") t.Arg = t.Has("arg") @@ -152,6 +158,14 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { 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") if t.PlaceHolder == "" { 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. 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] } // GetBool returns true if the given tag looks like a boolean truth string. 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. 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. 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. func (t *Tag) GetRune(k string) (rune, error) { - r, _ := utf8.DecodeRuneInString(t.items[k]) + r, _ := utf8.DecodeRuneInString(t.Get(k)) 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 } diff --git a/tag_test.go b/tag_test.go index efd3c92..5276788 100644 --- a/tag_test.go +++ b/tag_test.go @@ -1,6 +1,7 @@ package kong_test import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -109,3 +110,31 @@ func TestManySeps(t *testing.T) { require.NoError(t, err) 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.`) +}