cue/load: support injection of system variables

For reviewer: tests and naming are the big ticket
items to review here.

For instance:

   wd: string @tag(wd,var=cwd)

Fixes #222
os-specific filepath functionality available by passing
the "os" injection variable as second argument

Fixes #135
username:  available as the username injection variable
see cue help injection

Change-Id: I33c04f5f8dff34a1b6a4333a6674b3f36be48d34
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9578
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
index 9ad8a7c..1c4381c 100644
--- a/cmd/cue/cmd/common.go
+++ b/cmd/cue/cmd/common.go
@@ -416,6 +416,9 @@
 func setTags(f *pflag.FlagSet, cfg *load.Config) error {
 	tags, _ := f.GetStringArray(string(flagInject))
 	cfg.Tags = tags
+	if b, _ := f.GetBool(string(flagInjectVars)); b {
+		cfg.TagVars = load.DefaultTagVars()
+	}
 	return nil
 }
 
diff --git a/cmd/cue/cmd/flags.go b/cmd/cue/cmd/flags.go
index a9edfe4..e616eac 100644
--- a/cmd/cue/cmd/flags.go
+++ b/cmd/cue/cmd/flags.go
@@ -21,17 +21,18 @@
 
 // Common flags
 const (
-	flagAll       flagName = "all"
-	flagDryrun    flagName = "dryrun"
-	flagVerbose   flagName = "verbose"
-	flagAllErrors flagName = "all-errors"
-	flagTrace     flagName = "trace"
-	flagForce     flagName = "force"
-	flagIgnore    flagName = "ignore"
-	flagStrict    flagName = "strict"
-	flagSimplify  flagName = "simplify"
-	flagPackage   flagName = "package"
-	flagInject    flagName = "inject"
+	flagAll        flagName = "all"
+	flagDryrun     flagName = "dryrun"
+	flagVerbose    flagName = "verbose"
+	flagAllErrors  flagName = "all-errors"
+	flagTrace      flagName = "trace"
+	flagForce      flagName = "force"
+	flagIgnore     flagName = "ignore"
+	flagStrict     flagName = "strict"
+	flagSimplify   flagName = "simplify"
+	flagPackage    flagName = "package"
+	flagInject     flagName = "inject"
+	flagInjectVars flagName = "inject-vars"
 
 	flagExpression  flagName = "expression"
 	flagSchema      flagName = "schema"
@@ -90,6 +91,8 @@
 func addInjectionFlags(f *pflag.FlagSet, auto bool) {
 	f.StringArrayP(string(flagInject), "t", nil,
 		"set the value of a tagged field")
+	f.BoolP(string(flagInjectVars), "T", auto,
+		"inject system variables in tags")
 }
 
 type flagName string
diff --git a/cmd/cue/cmd/help.go b/cmd/cue/cmd/help.go
index aadbca6..d1d8578 100644
--- a/cmd/cue/cmd/help.go
+++ b/cmd/cue/cmd/help.go
@@ -331,6 +331,31 @@
    environment: "prod" | "staging" @tag(env,short=prod|staging)
 
 ensures the user may only specify "prod" or "staging".
+
+
+Tag variables
+
+The injection mechanism allows for the injection of system variables:
+when variable injection is enabled, tags of the form
+
+    @tag(dir,var=cwd)
+
+will inject the named variable (here cwd) into the tag. An explicitly
+set value for a tag using --inject/-t takes precedence over an
+available tag variable.
+
+The following variables are supported:
+
+   now        current time in RFC3339 format.
+   os         OS identifier of the current system. Valid values:
+                aix       android   darwin    dragonfly
+                freebsd   illumos   ios       js (wasm)
+                linux     netbsd    openbsd   plan9
+                solaris   windows
+   cwd        working directory
+   username   current username
+   hostname   current hostname
+   rand       a random 128-bit integer
 `,
 }
 
diff --git a/cmd/cue/cmd/testdata/script/help_cmd.txt b/cmd/cue/cmd/testdata/script/help_cmd.txt
index ab7de8e..2b76d25 100644
--- a/cmd/cue/cmd/testdata/script/help_cmd.txt
+++ b/cmd/cue/cmd/testdata/script/help_cmd.txt
@@ -140,6 +140,7 @@
 Flags:
   -h, --help                 help for cmd
   -t, --inject stringArray   set the value of a tagged field
+  -T, --inject-vars          inject system variables in tags (default true)
 
 Global Flags:
   -E, --all-errors   print all available errors
diff --git a/cmd/cue/cmd/testdata/script/help_cmd_flags.txt b/cmd/cue/cmd/testdata/script/help_cmd_flags.txt
index 8cad548..8993fed 100644
--- a/cmd/cue/cmd/testdata/script/help_cmd_flags.txt
+++ b/cmd/cue/cmd/testdata/script/help_cmd_flags.txt
@@ -138,6 +138,7 @@
 Flags:
   -h, --help                 help for cmd
   -t, --inject stringArray   set the value of a tagged field
+  -T, --inject-vars          inject system variables in tags (default true)
 
 Global Flags:
   -E, --all-errors   print all available errors
diff --git a/cmd/cue/cmd/testdata/script/inject.txt b/cmd/cue/cmd/testdata/script/inject.txt
index b2f4403..a6b9a3e 100644
--- a/cmd/cue/cmd/testdata/script/inject.txt
+++ b/cmd/cue/cmd/testdata/script/inject.txt
@@ -1,7 +1,21 @@
 cue eval test.cue -t env=prod
-
 cmp stdout expect-stdout
 
+cue eval vars.cue -T
+cmp stdout expect-stdout-vars
+
+cue eval vars.cue -T -t dir=xxx
+cmp stdout expect-stdout-override
+
+cue eval vars.cue
+cmp stdout expect-stdout-novars
+
+! cue eval -T err.cue
+cmp stderr expect-stderr-err
+
+cue cmd user vars.cue vars_tool.cue
+cmp stdout expect-stdout-tool
+
 # TODO: report errors for invalid tags?
 
 -- test.cue --
@@ -11,3 +25,39 @@
 
 -- expect-stdout --
 environment: "prod"
+-- vars.cue --
+import "path"
+
+_os: string @tag(os,var=os)
+_dir: string @tag(dir,var=cwd)
+
+base: path.Base(_dir, _os)
+
+-- err.cue --
+dir: string @tag(dir,var=userz)
+
+-- vars_tool.cue --
+import (
+    "path"
+    "tool/cli"
+)
+
+wd: string @tag(wd,var=cwd)
+_os: string @tag(os,var=os)
+
+command: user: {
+    base: cli.Print & { text: path.Base(wd, _os) }
+}
+
+-- expect-stdout-vars --
+base: "script-inject"
+-- expect-stderr-err --
+tag variable 'userz' not found
+-- expect-stdout-override --
+base: "xxx"
+-- expect-stdout-novars --
+import "path"
+
+base: path.Base(_dir, _os)
+-- expect-stdout-tool --
+script-inject
diff --git a/cue/load/config.go b/cue/load/config.go
index 847e447..517346e 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -218,6 +218,12 @@
 	// ensures the user may only specify "prod" or "staging".
 	Tags []string
 
+	// TagVars defines a set of key value pair the values of which may be
+	// referenced by tags.
+	//
+	// Use DefaultTagVars to get a pre-loaded map with supported values.
+	TagVars map[string]TagVar
+
 	// Include all files, regardless of tags.
 	AllCUEFiles bool
 
diff --git a/cue/load/loader.go b/cue/load/loader.go
index 1cdd0c1..72f58be 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -97,6 +97,7 @@
 		for _, p := range a {
 			p.ReportError(err)
 		}
+		return a
 	}
 
 	if l.replacements == nil {
@@ -135,7 +136,7 @@
 type loader struct {
 	cfg          *Config
 	stk          importStack
-	tags         []tag // tags found in files
+	tags         []*tag // tags found in files
 	buildTags    map[string]bool
 	replacements map[ast.Node]ast.Node
 }
diff --git a/cue/load/tags.go b/cue/load/tags.go
index 6692ac2..a9ab269 100644
--- a/cue/load/tags.go
+++ b/cue/load/tags.go
@@ -15,7 +15,13 @@
 package load
 
 import (
+	"crypto/rand"
+	"encoding/hex"
+	"os"
+	"os/user"
+	"runtime"
 	"strings"
+	"time"
 
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
@@ -27,6 +33,72 @@
 	"cuelang.org/go/internal/cli"
 )
 
+// A TagVar represents an injection variable.
+type TagVar struct {
+	// Func returns an ast for a tag variable. It is only called once
+	// per evaluation of a configuration.
+	Func func() (ast.Expr, error)
+
+	// Description documents this TagVar.
+	Description string
+}
+
+const rfc3339 = "2006-01-02T15:04:05.999999999Z"
+
+// DefaultTagVars creates a new map with a set of supported injection variables.
+func DefaultTagVars() map[string]TagVar {
+	return map[string]TagVar{
+		"now": {
+			Func: func() (ast.Expr, error) {
+				return ast.NewString(time.Now().UTC().Format(rfc3339)), nil
+			},
+		},
+		"os": {
+			Func: func() (ast.Expr, error) {
+				return ast.NewString(runtime.GOOS), nil
+			},
+		},
+		"cwd": {
+			Func: func() (ast.Expr, error) {
+				return varToString(os.Getwd())
+			},
+		},
+		"username": {
+			Func: func() (ast.Expr, error) {
+				u, err := user.Current()
+				return varToString(u.Username, err)
+			},
+		},
+		"hostname": {
+			Func: func() (ast.Expr, error) {
+				return varToString(os.Hostname())
+			},
+		},
+		"rand": {
+			Func: func() (ast.Expr, error) {
+				var b [16]byte
+				_, err := rand.Read(b[:])
+				if err != nil {
+					return nil, err
+				}
+				var hx [34]byte
+				hx[0] = '0'
+				hx[1] = 'x'
+				hex.Encode(hx[2:], b[:])
+				return ast.NewLit(token.INT, string(hx[:])), nil
+			},
+		},
+	}
+}
+
+func varToString(s string, err error) (ast.Expr, error) {
+	if err != nil {
+		return nil, err
+	}
+	x := ast.NewString(s)
+	return x, nil
+}
+
 // A tag binds an identifier to a field to allow passing command-line values.
 //
 // A tag is of the form
@@ -45,14 +117,17 @@
 // 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
+	key            string
+	kind           cue.Kind
+	shorthands     []string
+	vars           string // -T flag
+	hasReplacement bool
 
 	field *ast.Field
 }
 
-func parseTag(pos token.Pos, body string) (t tag, err errors.Error) {
+func parseTag(pos token.Pos, body string) (t *tag, err errors.Error) {
+	t = &tag{}
 	t.kind = cue.StringKind
 
 	a := internal.ParseAttrBody(pos, body)
@@ -85,27 +160,33 @@
 		}
 	}
 
+	if s, ok, _ := a.Lookup(1, "var"); ok {
+		t.vars = s
+	}
+
 	return t, nil
 }
 
 func (t *tag) inject(value string, l *loader) errors.Error {
 	e, err := cli.ParseValue(token.NoPos, t.key, value, t.kind)
-	if err != nil {
-		return err
-	}
-	injected := ast.NewBinExpr(token.AND, t.field.Value, e)
+	t.injectValue(e, l)
+	return err
+}
+
+func (t *tag) injectValue(x ast.Expr, l *loader) {
+	injected := ast.NewBinExpr(token.AND, t.field.Value, x)
 	if l.replacements == nil {
 		l.replacements = map[ast.Node]ast.Node{}
 	}
 	l.replacements[t.field.Value] = injected
 	t.field.Value = injected
-	return nil
+	t.hasReplacement = true
 }
 
 // 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) {
+func findTags(b *build.Instance) (tags []*tag, errs errors.Error) {
 	findInvalidTags := func(x ast.Node, msg string) {
 		ast.Walk(x, nil, func(n ast.Node) {
 			if f, ok := n.(*ast.Field); ok {
@@ -190,6 +271,35 @@
 			}
 		}
 	}
+
+	if l.cfg.TagVars != nil {
+		vars := map[string]ast.Expr{}
+
+		// Inject tag variables if the tag wasn't already set.
+		for _, t := range l.tags {
+			if t.hasReplacement || t.vars == "" {
+				continue
+			}
+			x, ok := vars[t.vars]
+			if !ok {
+				tv, ok := l.cfg.TagVars[t.vars]
+				if !ok {
+					return errors.Newf(token.NoPos,
+						"tag variable '%s' not found", t.vars)
+				}
+				tag, err := tv.Func()
+				if err != nil {
+					return errors.Wrapf(err, token.NoPos,
+						"error getting tag variable '%s'", t.vars)
+				}
+				x = tag
+				vars[t.vars] = tag
+			}
+			if x != nil {
+				t.injectValue(x, l)
+			}
+		}
+	}
 	return nil
 }
 
diff --git a/cue/load/tags_test.go b/cue/load/tags_test.go
new file mode 100644
index 0000000..122495f
--- /dev/null
+++ b/cue/load/tags_test.go
@@ -0,0 +1,126 @@
+// Copyright 2021 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 load
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/cuecontext"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/diff"
+)
+
+var testTagVars = map[string]TagVar{
+	"now":      stringVar("2006-01-02T15:04:05.999999999Z"),
+	"os":       stringVar("m1"),
+	"cwd":      stringVar("home"),
+	"username": stringVar("cueser"),
+	"hostname": stringVar("cuebe"),
+	"rand": {Func: func() (ast.Expr, error) {
+		return ast.NewLit(token.INT, "112950970371208119678246559335704039641"), nil
+	}},
+}
+
+func stringVar(s string) TagVar {
+	return TagVar{Func: func() (ast.Expr, error) { return ast.NewString(s), nil }}
+}
+
+func TestTags(t *testing.T) {
+	dir, _ := ioutil.TempDir("", "")
+	defer os.RemoveAll(dir)
+
+	testCases := []struct {
+		in  string
+		out string
+		err string
+	}{{
+		in: `
+		rand: int    @tag(foo,var=rand)
+		time: string @tag(bar,var=now)
+		host: string @tag(bar,var=hostname)
+		user: string @tag(bar,var=username)
+		cwd:  string @tag(bar,var=cwd)
+		`,
+
+		out: `{
+			rand: 112950970371208119678246559335704039641
+			time: "2006-01-02T15:04:05.999999999Z"
+			host: "cuebe"
+			user: "cueser"
+			cwd:  "home"
+		}`,
+	}, {
+		in: `
+		time: int @tag(bar,var=now)
+		`,
+		err: `time: conflicting values int and "2006-01-02T15:04:05.999999999Z" (mismatched types int and string)`,
+	}, {
+		// Auto inject only on marked places
+		// TODO: Is this the right thing to do?
+		in: `
+			u1: string @tag(bar,var=username)
+			u2: string @tag(bar)
+			`,
+		out: `{
+			u1: "cueser"
+            u2: string // not filled
+        }`,
+	}, {
+		in: `
+		u1: string @tag(bar,var=user)
+		`,
+		err: `tag variable 'user' not found`,
+	}}
+
+	for _, tc := range testCases {
+		t.Run("", func(t *testing.T) {
+			cfg := &Config{
+				Dir: dir,
+				Overlay: map[string]Source{
+					filepath.Join(dir, "foo.cue"): FromString(tc.in),
+				},
+				TagVars: testTagVars,
+			}
+			b := Instances([]string{"foo.cue"}, cfg)[0]
+
+			c := cuecontext.New()
+			got := c.BuildInstance(b)
+			switch err := got.Err(); {
+			case (err == nil) != (tc.err == ""):
+				t.Fatalf("error: got %v; want %v", err, tc.err)
+
+			case err != nil:
+				got := err.Error()
+				if got != tc.err {
+					t.Fatalf("error: got %v; want %v", got, tc.err)
+				}
+
+			default:
+				want := c.CompileString(tc.out)
+				if !got.Equals(want) {
+					_, es := diff.Diff(got, want)
+					b := &bytes.Buffer{}
+					diff.Print(b, es)
+					t.Error(b)
+				}
+			}
+		})
+	}
+}