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.