cmd/cue/cmd: add --tags flag
The tags flag allows specifying values for fields.
Issue #190
Issue #159
Change-Id: Iddbfe8eb9fcb2a163ce773411042e020372ff8be
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4949
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/cmd.go b/cmd/cue/cmd/cmd.go
index 333239e..8318f54 100644
--- a/cmd/cue/cmd/cmd.go
+++ b/cmd/cue/cmd/cmd.go
@@ -24,7 +24,7 @@
// TODO: generate long description from documentation.
func newCmdCmd(c *Command) *cobra.Command {
- return &cobra.Command{
+ cmd := &cobra.Command{
Use: "cmd <name> [-x] [instances]",
Short: "run a user-defined shell command",
Long: `cmd executes defined the named command for each of the named instances.
@@ -231,4 +231,10 @@
return nil
}),
}
+
+ cmd.Flags().SetInterspersed(false)
+ cmd.Flags().StringArrayP(string(flagTags), "t", nil,
+ "set the value of a tagged field")
+
+ return cmd
}
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
index 8429b6d..e7465ea 100644
--- a/cmd/cue/cmd/common.go
+++ b/cmd/cue/cmd/common.go
@@ -104,6 +104,7 @@
if binst == nil {
return nil
}
+ decorateInstances(cmd, flagTags.StringArray(cmd), binst)
return buildInstances(cmd, binst)
}
@@ -112,6 +113,7 @@
if len(binst) == 0 {
return nil
}
+
return binst
}
@@ -126,6 +128,8 @@
exitIfErr(cmd, inst, inst.Err, true)
}
+ decorateInstances(cmd, flagTags.StringArray(cmd), binst)
+
if flagIgnore.Bool(cmd) {
return instances
}
@@ -156,12 +160,11 @@
return instances, nil
}
-func buildTools(cmd *Command, args []string) (*cue.Instance, error) {
+func buildTools(cmd *Command, tags, args []string) (*cue.Instance, error) {
binst := loadFromArgs(cmd, args, &load.Config{Tools: true})
if len(binst) == 0 {
return nil, nil
}
-
included := map[string]bool{}
ti := binst[0].Context().NewInstance(binst[0].Root, nil)
@@ -173,6 +176,7 @@
}
}
}
+ decorateInstances(cmd, tags, append(binst, ti))
insts, err := buildToolInstances(cmd, binst)
if err != nil {
diff --git a/cmd/cue/cmd/eval.go b/cmd/cue/cmd/eval.go
index 89c579f..0a28c89 100644
--- a/cmd/cue/cmd/eval.go
+++ b/cmd/cue/cmd/eval.go
@@ -68,6 +68,9 @@
cmd.Flags().BoolP(string(flagAll), "a", false,
"show optional and hidden fields")
+ cmd.Flags().StringArrayP(string(flagTags), "t", nil,
+ "set the value of a tagged field")
+
// TODO: Option to include comments in output.
return cmd
}
diff --git a/cmd/cue/cmd/export.go b/cmd/cue/cmd/export.go
index 9f6c09e..4a65036 100644
--- a/cmd/cue/cmd/export.go
+++ b/cmd/cue/cmd/export.go
@@ -93,6 +93,9 @@
flagMedia.Add(cmd)
cmd.Flags().Bool(string(flagEscape), false, "use HTML escaping")
+ cmd.Flags().StringArrayP(string(flagTags), "t", nil,
+ "set the value of a tagged field")
+
return cmd
}
diff --git a/cmd/cue/cmd/flags.go b/cmd/cue/cmd/flags.go
index ee20752..92ad727 100644
--- a/cmd/cue/cmd/flags.go
+++ b/cmd/cue/cmd/flags.go
@@ -30,6 +30,7 @@
flagSimplify flagName = "simplify"
flagPackage flagName = "package"
flagDebug flagName = "debug"
+ flagTags flagName = "tags"
flagExpression flagName = "expression"
flagEscape flagName = "escape"
diff --git a/cmd/cue/cmd/root.go b/cmd/cue/cmd/root.go
index 310571b..83a4084 100644
--- a/cmd/cue/cmd/root.go
+++ b/cmd/cue/cmd/root.go
@@ -225,10 +225,10 @@
cmd = newRootCmd()
rootCmd := cmd.root
- rootCmd.SetArgs(args)
if len(args) == 0 {
return cmd, nil
}
+ rootCmd.SetArgs(args)
var sub = map[string]*subSpec{
"cmd": {commandSection, cmd.cmd},
@@ -238,14 +238,27 @@
if args[0] == "help" {
// Allow errors.
- _ = addSubcommands(cmd, sub, args[1:])
+ _ = addSubcommands(cmd, sub, args[1:], true)
return cmd, nil
}
if _, ok := sub[args[0]]; ok {
- return cmd, addSubcommands(cmd, sub, args)
+ return cmd, addSubcommands(cmd, sub, args, false)
}
+ // TODO: clean this up once we either use Cobra properly or when we remove
+ // it.
+ err = cmd.cmd.ParseFlags(args)
+ if err != nil {
+ return nil, err
+ }
+ tags, err := cmd.cmd.Flags().GetStringArray(string(flagTags))
+ if err != nil {
+ return nil, err
+ }
+ args = cmd.cmd.Flags().Args()
+ rootCmd.SetArgs(args)
+
if c, _, err := rootCmd.Find(args); err == nil && c != nil {
return cmd, nil
}
@@ -254,7 +267,7 @@
return cmd, nil // Forces unknown command message from Cobra.
}
- tools, err := buildTools(cmd, args[1:])
+ tools, err := buildTools(cmd, tags, args[1:])
if err != nil {
return cmd, err
}
@@ -276,10 +289,25 @@
cmd *cobra.Command
}
-func addSubcommands(cmd *Command, sub map[string]*subSpec, args []string) error {
+func addSubcommands(cmd *Command, sub map[string]*subSpec, args []string, isHelp bool) error {
+ var tags []string
if len(args) > 0 {
if _, ok := sub[args[0]]; ok {
+ oldargs := []string{args[0]}
args = args[1:]
+
+ if !isHelp {
+ err := cmd.cmd.ParseFlags(args)
+ if err != nil {
+ return err
+ }
+ tags, err = cmd.cmd.Flags().GetStringArray(string(flagTags))
+ if err != nil {
+ return err
+ }
+ args = cmd.cmd.Flags().Args()
+ cmd.root.SetArgs(append(oldargs, args...))
+ }
}
}
@@ -290,7 +318,7 @@
args = args[1:]
}
- tools, err := buildTools(cmd, args)
+ tools, err := buildTools(cmd, tags, args)
if err != nil {
return err
}
diff --git a/cmd/cue/cmd/script_test.go b/cmd/cue/cmd/script_test.go
index 13b7097..c66e694 100644
--- a/cmd/cue/cmd/script_test.go
+++ b/cmd/cue/cmd/script_test.go
@@ -1,8 +1,11 @@
package cmd
import (
+ "bufio"
"bytes"
+ "context"
"fmt"
+ "io/ioutil"
"os"
"path"
"path/filepath"
@@ -59,6 +62,59 @@
})
}
+// TestScriptDebug takes a single testscript file and then runs it within the
+// same process so it can be used for debugging. It runs the first cue command
+// it finds.
+//
+// Usage Comment out t.Skip() and set file to test.
+func TestX(t *testing.T) {
+ t.Skip()
+ const path = "./testdata/script/help_hello.txt"
+
+ check := func(err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ tmpdir, err := ioutil.TempDir("", "cue-script")
+ check(err)
+ defer os.Remove(tmpdir)
+
+ a, err := txtar.ParseFile(filepath.FromSlash(path))
+ check(err)
+
+ for _, f := range a.Files {
+ name := filepath.Join(tmpdir, f.Name)
+ check(os.MkdirAll(filepath.Dir(name), 0777))
+ check(ioutil.WriteFile(name, f.Data, 0666))
+ }
+
+ cwd, err := os.Getwd()
+ check(err)
+ defer os.Chdir(cwd)
+ _ = os.Chdir(tmpdir)
+
+ for s := bufio.NewScanner(bytes.NewReader(a.Comment)); s.Scan(); {
+ cmd := s.Text()
+ cmd = strings.TrimLeft(cmd, "! ")
+ if !strings.HasPrefix(cmd, "cue ") {
+ continue
+ }
+
+ c, err := New(strings.Split(cmd, " ")[1:])
+ check(err)
+ b := &bytes.Buffer{}
+ c.SetOutput(b)
+ err = c.Run(context.Background())
+ // Always create an error to show
+ t.Error(err, "\n", b.String())
+ return
+ }
+ t.Fatal("NO COMMAND FOUND")
+}
+
func TestMain(m *testing.M) {
// Setting inTest causes filenames printed in error messages
// to be normalized so the output looks the same on Unix
diff --git a/cmd/cue/cmd/tags.go b/cmd/cue/cmd/tags.go
new file mode 100644
index 0000000..00912ef
--- /dev/null
+++ b/cmd/cue/cmd/tags.go
@@ -0,0 +1,191 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "strings"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/build"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal"
+ "cuelang.org/go/internal/cli"
+)
+
+func decorateInstances(cmd *Command, tags []string, a []*build.Instance) {
+ if len(tags) == 0 {
+ return
+ }
+ exitOnErr(cmd, injectTags(tags, a), true)
+}
+
+// A tag binds an identifier to a field to allow passing command-line values.
+//
+// A tag is of the form
+// @tag(<name>,[type=(string|int|number|bool)][,short=<shorthand>+])
+//
+// The name is mandatory and type defaults to string. Tags are set using the -t
+// option on the command line. -t name=value will parse value for the type
+// defined for name and set the field for which this tag was defined to this
+// value. A tag may be associated with multiple fields.
+//
+// Tags also allow shorthands. If a shorthand bar is declared for a tag with
+// name foo, then -t bar is identical to -t foo=bar.
+//
+// It is a deliberate choice to not allow other values to be associated with
+// shorthands than the shorthand name itself. Doing so would create a powerful
+// mechanism that would assign different values to different fields based on the
+// same shorthand, duplicating functionality that is already available in CUE.
+type tag struct {
+ key string
+ kind cue.Kind
+ shorthands []string
+
+ field *ast.Field
+}
+
+func parseTag(pos token.Pos, body string) (t tag, err errors.Error) {
+ t.kind = cue.StringKind
+
+ a := internal.ParseAttrBody(pos, body)
+
+ t.key, _ = a.String(0)
+ if !ast.IsValidIdent(t.key) {
+ return t, errors.Newf(pos, "invalid identifier %q", t.key)
+ }
+
+ if s, ok, _ := a.Lookup(1, "type"); ok {
+ switch s {
+ case "string":
+ case "int":
+ t.kind = cue.IntKind
+ case "number":
+ t.kind = cue.NumberKind
+ case "bool":
+ t.kind = cue.BoolKind
+ default:
+ return t, errors.Newf(pos, "invalid type %q", s)
+ }
+ }
+
+ if s, ok, _ := a.Lookup(1, "short"); ok {
+ for _, s := range strings.Split(s, "|") {
+ if !ast.IsValidIdent(t.key) {
+ return t, errors.Newf(pos, "invalid identifier %q", s)
+ }
+ t.shorthands = append(t.shorthands, s)
+ }
+ }
+
+ return t, nil
+}
+
+func (t *tag) inject(value string) errors.Error {
+ e, err := cli.ParseValue(token.NoPos, t.key, value, t.kind)
+ if err != nil {
+ return err
+ }
+ t.field.Value = ast.NewBinExpr(token.AND, t.field.Value, e)
+ return nil
+}
+
+// findTags defines which fields may be associated with tags.
+//
+// TODO: should we limit the depth at which tags may occur?
+func findTags(b *build.Instance) (tags []tag, errs errors.Error) {
+ for _, f := range b.Files {
+ ast.Walk(f, func(n ast.Node) bool {
+ if b.Err != nil {
+ return false
+ }
+
+ switch x := n.(type) {
+ case *ast.StructLit, *ast.File:
+ return true
+
+ case *ast.Field:
+ // TODO: allow optional fields?
+ _, _, err := ast.LabelName(x.Label)
+ if err != nil || x.Optional != token.NoPos {
+ return false
+ }
+
+ for _, a := range x.Attrs {
+ key, body := a.Split()
+ if key != "tag" {
+ continue
+ }
+ t, err := parseTag(a.Pos(), body)
+ if err != nil {
+ errs = errors.Append(errs, err)
+ continue
+ }
+ t.field = x
+ tags = append(tags, t)
+ }
+ return true
+ }
+ return false
+ }, nil)
+ }
+ return tags, errs
+}
+
+func injectTags(tags []string, b []*build.Instance) errors.Error {
+ var a []tag
+ for _, p := range b {
+ x, err := findTags(p)
+ if err != nil {
+ return err
+ }
+ a = append(a, x...)
+ }
+
+ // Parses command line args
+ for _, s := range tags {
+ p := strings.Index(s, "=")
+ found := false
+ if p > 0 { // key-value
+ for _, t := range a {
+ if t.key == s[:p] {
+ found = true
+ if err := t.inject(s[p+1:]); err != nil {
+ return err
+ }
+ }
+ }
+ if !found {
+ return errors.Newf(token.NoPos, "no tag for %q", s[:p])
+ }
+ } else { // shorthand
+ for _, t := range a {
+ for _, sh := range t.shorthands {
+ if sh == s {
+ found = true
+ if err := t.inject(s); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ if !found {
+ return errors.Newf(token.NoPos, "no shorthand for %q", s)
+ }
+ }
+ }
+ return nil
+}
diff --git a/cmd/cue/cmd/testdata/script/cmd_tags.txt b/cmd/cue/cmd/testdata/script/cmd_tags.txt
new file mode 100644
index 0000000..2efaed2
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/cmd_tags.txt
@@ -0,0 +1,19 @@
+cue cmd -t prod -t name=bar tag tags.cue tags_tool.cue
+cmp stdout expect-stdout
+
+-- expect-stdout --
+prod: bar
+-- tags.cue --
+package tags
+
+var: env: "prod" | "staging" @tag(env,short=prod|staging)
+var: name: string @tag(name)
+
+-- tags_tool.cue --
+package tags
+
+import "tool/cli"
+
+command: tag: cli.Print & {
+ text: "\(var.env): \(var.name)"
+}
diff --git a/cmd/cue/cmd/testdata/script/eval_tags.txt b/cmd/cue/cmd/testdata/script/eval_tags.txt
new file mode 100644
index 0000000..c114407
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/eval_tags.txt
@@ -0,0 +1,18 @@
+cue eval -t env=staging -t name=bar
+cmp stdout expect-stdout
+
+-- expect-stdout --
+var: {
+ name: "bar"
+ env: "staging"
+}
+-- tags.cue --
+package tags
+
+
+var: env: "prod" | "staging" @tag(env,short=prod|staging)
+var: name: string @tag(name)
+
+// This is prohibited as for now.
+// foo: [string]: string @tag(all)
+// foo: bar: string
diff --git a/cmd/cue/cmd/testdata/script/help_cmd.txt b/cmd/cue/cmd/testdata/script/help_cmd.txt
index ac15b70..4da924c 100644
--- a/cmd/cue/cmd/testdata/script/help_cmd.txt
+++ b/cmd/cue/cmd/testdata/script/help_cmd.txt
@@ -221,7 +221,8 @@
hello say hello to someone
Flags:
- -h, --help help for cmd
+ -h, --help help for cmd
+ -t, --tags stringArray set the value of a tagged field
Global Flags:
--debug give detailed error info
diff --git a/cmd/cue/cmd/vet.go b/cmd/cue/cmd/vet.go
index 28cab4d..07bd442 100644
--- a/cmd/cue/cmd/vet.go
+++ b/cmd/cue/cmd/vet.go
@@ -82,6 +82,9 @@
cmd.Flags().StringArrayP(string(flagExpression), "e", nil,
"use this expression to validate non-CUE files")
+ cmd.Flags().StringArrayP(string(flagTags), "t", nil,
+ "set the value of a tagged field")
+
return cmd
}
@@ -90,6 +93,7 @@
if builds == nil {
return nil
}
+ decorateInstances(cmd, flagTags.StringArray(cmd), builds)
instances := buildInstances(cmd, builds)
// Go into a special vet mode if the user explicitly specified non-cue
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index f4042b3..069926d 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -290,6 +290,15 @@
func (a *Attribute) pos() *token.Pos { return &a.At }
func (a *Attribute) End() token.Pos { return a.At.Add(len(a.Text)) }
+func (a *Attribute) Split() (key, body string) {
+ s := a.Text
+ p := strings.IndexByte(s, '(')
+ if p < 0 || !strings.HasPrefix(s, "@") || !strings.HasSuffix(s, ")") {
+ return "", ""
+ }
+ return a.Text[1:p], a.Text[p+1 : len(s)-1]
+}
+
// A Field represents a field declaration in a struct.
type Field struct {
Label Label // must have at least one element.