Skip to content

Commit bcdf277

Browse files
committed
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 <[email protected]> Reviewed-by: Marcel van Lohuizen <[email protected]>
1 parent 1f618f0 commit bcdf277

File tree

10 files changed

+349
-23
lines changed

10 files changed

+349
-23
lines changed

cmd/cue/cmd/common.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ func (p *buildPlan) matchFile(file string) bool {
416416
func setTags(f *pflag.FlagSet, cfg *load.Config) error {
417417
tags, _ := f.GetStringArray(string(flagInject))
418418
cfg.Tags = tags
419+
if b, _ := f.GetBool(string(flagInjectVars)); b {
420+
cfg.TagVars = load.DefaultTagVars()
421+
}
419422
return nil
420423
}
421424

cmd/cue/cmd/flags.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ import (
2121

2222
// Common flags
2323
const (
24-
flagAll flagName = "all"
25-
flagDryrun flagName = "dryrun"
26-
flagVerbose flagName = "verbose"
27-
flagAllErrors flagName = "all-errors"
28-
flagTrace flagName = "trace"
29-
flagForce flagName = "force"
30-
flagIgnore flagName = "ignore"
31-
flagStrict flagName = "strict"
32-
flagSimplify flagName = "simplify"
33-
flagPackage flagName = "package"
34-
flagInject flagName = "inject"
24+
flagAll flagName = "all"
25+
flagDryrun flagName = "dryrun"
26+
flagVerbose flagName = "verbose"
27+
flagAllErrors flagName = "all-errors"
28+
flagTrace flagName = "trace"
29+
flagForce flagName = "force"
30+
flagIgnore flagName = "ignore"
31+
flagStrict flagName = "strict"
32+
flagSimplify flagName = "simplify"
33+
flagPackage flagName = "package"
34+
flagInject flagName = "inject"
35+
flagInjectVars flagName = "inject-vars"
3536

3637
flagExpression flagName = "expression"
3738
flagSchema flagName = "schema"
@@ -90,6 +91,8 @@ func addOrphanFlags(f *pflag.FlagSet) {
9091
func addInjectionFlags(f *pflag.FlagSet, auto bool) {
9192
f.StringArrayP(string(flagInject), "t", nil,
9293
"set the value of a tagged field")
94+
f.BoolP(string(flagInjectVars), "T", auto,
95+
"inject system variables in tags")
9396
}
9497

9598
type flagName string

cmd/cue/cmd/help.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,31 @@ field. For instance
331331
environment: "prod" | "staging" @tag(env,short=prod|staging)
332332
333333
ensures the user may only specify "prod" or "staging".
334+
335+
336+
Tag variables
337+
338+
The injection mechanism allows for the injection of system variables:
339+
when variable injection is enabled, tags of the form
340+
341+
@tag(dir,var=cwd)
342+
343+
will inject the named variable (here cwd) into the tag. An explicitly
344+
set value for a tag using --inject/-t takes precedence over an
345+
available tag variable.
346+
347+
The following variables are supported:
348+
349+
now current time in RFC3339 format.
350+
os OS identifier of the current system. Valid values:
351+
aix android darwin dragonfly
352+
freebsd illumos ios js (wasm)
353+
linux netbsd openbsd plan9
354+
solaris windows
355+
cwd working directory
356+
username current username
357+
hostname current hostname
358+
rand a random 128-bit integer
334359
`,
335360
}
336361

cmd/cue/cmd/testdata/script/help_cmd.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ Available Commands:
140140
Flags:
141141
-h, --help help for cmd
142142
-t, --inject stringArray set the value of a tagged field
143+
-T, --inject-vars inject system variables in tags (default true)
143144

144145
Global Flags:
145146
-E, --all-errors print all available errors

cmd/cue/cmd/testdata/script/help_cmd_flags.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Usage:
138138
Flags:
139139
-h, --help help for cmd
140140
-t, --inject stringArray set the value of a tagged field
141+
-T, --inject-vars inject system variables in tags (default true)
141142

142143
Global Flags:
143144
-E, --all-errors print all available errors

cmd/cue/cmd/testdata/script/inject.txt

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
cue eval test.cue -t env=prod
2-
32
cmp stdout expect-stdout
43

4+
cue eval vars.cue -T
5+
cmp stdout expect-stdout-vars
6+
7+
cue eval vars.cue -T -t dir=xxx
8+
cmp stdout expect-stdout-override
9+
10+
cue eval vars.cue
11+
cmp stdout expect-stdout-novars
12+
13+
! cue eval -T err.cue
14+
cmp stderr expect-stderr-err
15+
16+
cue cmd user vars.cue vars_tool.cue
17+
cmp stdout expect-stdout-tool
18+
519
# TODO: report errors for invalid tags?
620

721
-- test.cue --
@@ -11,3 +25,39 @@ cmp stdout expect-stdout
1125

1226
-- expect-stdout --
1327
environment: "prod"
28+
-- vars.cue --
29+
import "path"
30+
31+
_os: string @tag(os,var=os)
32+
_dir: string @tag(dir,var=cwd)
33+
34+
base: path.Base(_dir, _os)
35+
36+
-- err.cue --
37+
dir: string @tag(dir,var=userz)
38+
39+
-- vars_tool.cue --
40+
import (
41+
"path"
42+
"tool/cli"
43+
)
44+
45+
wd: string @tag(wd,var=cwd)
46+
_os: string @tag(os,var=os)
47+
48+
command: user: {
49+
base: cli.Print & { text: path.Base(wd, _os) }
50+
}
51+
52+
-- expect-stdout-vars --
53+
base: "script-inject"
54+
-- expect-stderr-err --
55+
tag variable 'userz' not found
56+
-- expect-stdout-override --
57+
base: "xxx"
58+
-- expect-stdout-novars --
59+
import "path"
60+
61+
base: path.Base(_dir, _os)
62+
-- expect-stdout-tool --
63+
script-inject

cue/load/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ type Config struct {
218218
// ensures the user may only specify "prod" or "staging".
219219
Tags []string
220220

221+
// TagVars defines a set of key value pair the values of which may be
222+
// referenced by tags.
223+
//
224+
// Use DefaultTagVars to get a pre-loaded map with supported values.
225+
TagVars map[string]TagVar
226+
221227
// Include all files, regardless of tags.
222228
AllCUEFiles bool
223229

cue/load/loader.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func Instances(args []string, c *Config) []*build.Instance {
9797
for _, p := range a {
9898
p.ReportError(err)
9999
}
100+
return a
100101
}
101102

102103
if l.replacements == nil {
@@ -135,7 +136,7 @@ const (
135136
type loader struct {
136137
cfg *Config
137138
stk importStack
138-
tags []tag // tags found in files
139+
tags []*tag // tags found in files
139140
buildTags map[string]bool
140141
replacements map[ast.Node]ast.Node
141142
}

cue/load/tags.go

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
package load
1616

1717
import (
18+
"crypto/rand"
19+
"encoding/hex"
20+
"os"
21+
"os/user"
22+
"runtime"
1823
"strings"
24+
"time"
1925

2026
"cuelang.org/go/cue"
2127
"cuelang.org/go/cue/ast"
@@ -27,6 +33,72 @@ import (
2733
"cuelang.org/go/internal/cli"
2834
)
2935

36+
// A TagVar represents an injection variable.
37+
type TagVar struct {
38+
// Func returns an ast for a tag variable. It is only called once
39+
// per evaluation of a configuration.
40+
Func func() (ast.Expr, error)
41+
42+
// Description documents this TagVar.
43+
Description string
44+
}
45+
46+
const rfc3339 = "2006-01-02T15:04:05.999999999Z"
47+
48+
// DefaultTagVars creates a new map with a set of supported injection variables.
49+
func DefaultTagVars() map[string]TagVar {
50+
return map[string]TagVar{
51+
"now": {
52+
Func: func() (ast.Expr, error) {
53+
return ast.NewString(time.Now().UTC().Format(rfc3339)), nil
54+
},
55+
},
56+
"os": {
57+
Func: func() (ast.Expr, error) {
58+
return ast.NewString(runtime.GOOS), nil
59+
},
60+
},
61+
"cwd": {
62+
Func: func() (ast.Expr, error) {
63+
return varToString(os.Getwd())
64+
},
65+
},
66+
"username": {
67+
Func: func() (ast.Expr, error) {
68+
u, err := user.Current()
69+
return varToString(u.Username, err)
70+
},
71+
},
72+
"hostname": {
73+
Func: func() (ast.Expr, error) {
74+
return varToString(os.Hostname())
75+
},
76+
},
77+
"rand": {
78+
Func: func() (ast.Expr, error) {
79+
var b [16]byte
80+
_, err := rand.Read(b[:])
81+
if err != nil {
82+
return nil, err
83+
}
84+
var hx [34]byte
85+
hx[0] = '0'
86+
hx[1] = 'x'
87+
hex.Encode(hx[2:], b[:])
88+
return ast.NewLit(token.INT, string(hx[:])), nil
89+
},
90+
},
91+
}
92+
}
93+
94+
func varToString(s string, err error) (ast.Expr, error) {
95+
if err != nil {
96+
return nil, err
97+
}
98+
x := ast.NewString(s)
99+
return x, nil
100+
}
101+
30102
// A tag binds an identifier to a field to allow passing command-line values.
31103
//
32104
// A tag is of the form
@@ -45,14 +117,17 @@ import (
45117
// mechanism that would assign different values to different fields based on the
46118
// same shorthand, duplicating functionality that is already available in CUE.
47119
type tag struct {
48-
key string
49-
kind cue.Kind
50-
shorthands []string
120+
key string
121+
kind cue.Kind
122+
shorthands []string
123+
vars string // -T flag
124+
hasReplacement bool
51125

52126
field *ast.Field
53127
}
54128

55-
func parseTag(pos token.Pos, body string) (t tag, err errors.Error) {
129+
func parseTag(pos token.Pos, body string) (t *tag, err errors.Error) {
130+
t = &tag{}
56131
t.kind = cue.StringKind
57132

58133
a := internal.ParseAttrBody(pos, body)
@@ -85,27 +160,33 @@ func parseTag(pos token.Pos, body string) (t tag, err errors.Error) {
85160
}
86161
}
87162

163+
if s, ok, _ := a.Lookup(1, "var"); ok {
164+
t.vars = s
165+
}
166+
88167
return t, nil
89168
}
90169

91170
func (t *tag) inject(value string, l *loader) errors.Error {
92171
e, err := cli.ParseValue(token.NoPos, t.key, value, t.kind)
93-
if err != nil {
94-
return err
95-
}
96-
injected := ast.NewBinExpr(token.AND, t.field.Value, e)
172+
t.injectValue(e, l)
173+
return err
174+
}
175+
176+
func (t *tag) injectValue(x ast.Expr, l *loader) {
177+
injected := ast.NewBinExpr(token.AND, t.field.Value, x)
97178
if l.replacements == nil {
98179
l.replacements = map[ast.Node]ast.Node{}
99180
}
100181
l.replacements[t.field.Value] = injected
101182
t.field.Value = injected
102-
return nil
183+
t.hasReplacement = true
103184
}
104185

105186
// findTags defines which fields may be associated with tags.
106187
//
107188
// TODO: should we limit the depth at which tags may occur?
108-
func findTags(b *build.Instance) (tags []tag, errs errors.Error) {
189+
func findTags(b *build.Instance) (tags []*tag, errs errors.Error) {
109190
findInvalidTags := func(x ast.Node, msg string) {
110191
ast.Walk(x, nil, func(n ast.Node) {
111192
if f, ok := n.(*ast.Field); ok {
@@ -190,6 +271,35 @@ func injectTags(tags []string, l *loader) errors.Error {
190271
}
191272
}
192273
}
274+
275+
if l.cfg.TagVars != nil {
276+
vars := map[string]ast.Expr{}
277+
278+
// Inject tag variables if the tag wasn't already set.
279+
for _, t := range l.tags {
280+
if t.hasReplacement || t.vars == "" {
281+
continue
282+
}
283+
x, ok := vars[t.vars]
284+
if !ok {
285+
tv, ok := l.cfg.TagVars[t.vars]
286+
if !ok {
287+
return errors.Newf(token.NoPos,
288+
"tag variable '%s' not found", t.vars)
289+
}
290+
tag, err := tv.Func()
291+
if err != nil {
292+
return errors.Wrapf(err, token.NoPos,
293+
"error getting tag variable '%s'", t.vars)
294+
}
295+
x = tag
296+
vars[t.vars] = tag
297+
}
298+
if x != nil {
299+
t.injectValue(x, l)
300+
}
301+
}
302+
}
193303
return nil
194304
}
195305

0 commit comments

Comments
 (0)