pkg/tool/os: add tasks for environment variables
These are implemented as tasks as they are state
altering and may yield different results on repeated
calls.
Also:
- Remove usage of package errors for pkgs.
- Updated go-cmp
- Fix bug in dependency analysis where default
values could cause a dependency to be concrete
prematurely.
Issue #159
Change-Id: I517eb6892cbeff538c806a822510ffce5dcb31b0
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4461
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/pkg/tool/os/doc.go b/pkg/tool/os/doc.go
new file mode 100644
index 0000000..b32321f
--- /dev/null
+++ b/pkg/tool/os/doc.go
@@ -0,0 +1,48 @@
+// Code generated by cue get go. DO NOT EDIT.
+
+// Package os defines tasks for retrieving os-related information.
+//
+// CUE definitions:
+//
+// // A Value are all possible values allowed in flags.
+// // A null value unsets an environment variable.
+// Value :: bool | number | *string | null
+//
+// // Name indicates a valid flag name.
+// Name :: !="" & !~"^[$]"
+//
+// // Setenv defines a set of command line flags, the values of which will be set
+// // at run time. The doc comment of the flag is presented to the user in help.
+// //
+// // To define a shorthand, define the shorthand as a new flag referring to
+// // the flag of which it is a shorthand.
+// Setenv: {
+// $id: "tool/os.Setenv"
+//
+// [Name]: Value
+// }
+//
+// // Getenv gets and parses the specific command line variables.
+// Getenv: {
+// $id: "tool/os.Getenv"
+//
+// [Name]: Value
+// }
+//
+// // Environ populates a struct with all environment variables.
+// Environ: {
+// $id: "tool/os.Environ"
+//
+// // A map of all populated values.
+// // Individual entries may be specified ahead of time to enable
+// // validation and parsing. Values that are marked as required
+// // will fail the task if they are not found.
+// [Name]: Value
+// }
+//
+// // Clearenv clears all environment variables.
+// Clearenv: {
+// $id: "tool/os.Clearenv"
+// }
+//
+package os
diff --git a/pkg/tool/os/env.go b/pkg/tool/os/env.go
new file mode 100644
index 0000000..487d397
--- /dev/null
+++ b/pkg/tool/os/env.go
@@ -0,0 +1,274 @@
+// Copyright 2019 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 os
+
+//go:generate go run gen.go
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal/task"
+)
+
+func init() {
+ task.Register("tool/os.Setenv", newSetenvCmd)
+ task.Register("tool/os.Getenv", newGetenvCmd)
+ task.Register("tool/os.Environ", newEnvironCmd)
+ task.Register("tool/os.Clearenv", newClearenvCmd)
+
+ // TODO:
+ // Tasks:
+ // - Exit?
+ // - Getwd/ Setwd (or in tool/file?)
+
+ // Functions:
+ // - Hostname
+ // - UserCache/Home/Config (or in os/user?)
+}
+
+type clearenvCmd struct{}
+
+func newClearenvCmd(v cue.Value) (task.Runner, error) {
+ return &clearenvCmd{}, nil
+}
+
+func (c *clearenvCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+ os.Clearenv()
+ return map[string]interface{}{}, nil
+}
+
+type setenvCmd struct{}
+
+func newSetenvCmd(v cue.Value) (task.Runner, error) {
+ return &setenvCmd{}, nil
+}
+
+func (c *setenvCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+ iter, err := v.Fields()
+ if err != nil {
+ return nil, err
+ }
+
+ for iter.Next() {
+ name := iter.Label()
+ if strings.HasPrefix(name, "$") {
+ continue
+ }
+
+ v, _ := iter.Value().Default()
+
+ if !v.IsConcrete() {
+ return nil, errors.Newf(v.Pos(),
+ "non-concrete environment variable %s", name)
+ }
+ switch k := v.IncompleteKind(); k {
+ case cue.ListKind, cue.StructKind:
+ return nil, errors.Newf(v.Pos(),
+ "unsupported type %s for environment variable %s", k, name)
+
+ case cue.NullKind:
+ err = os.Unsetenv(name)
+
+ case cue.BoolKind:
+ if b, _ := v.Bool(); b {
+ err = os.Setenv(name, "1")
+ } else {
+ err = os.Setenv(name, "0")
+ }
+
+ case cue.StringKind:
+ s, _ := v.String()
+ err = os.Setenv(name, s)
+
+ default:
+ err = os.Setenv(name, fmt.Sprint(v))
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return map[string]interface{}{}, err
+}
+
+type getenvCmd struct{}
+
+func newGetenvCmd(v cue.Value) (task.Runner, error) {
+ return &getenvCmd{}, nil
+}
+
+func (c *getenvCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+ iter, err := v.Fields()
+ if err != nil {
+ return nil, err
+ }
+
+ update := map[string]interface{}{}
+
+ for iter.Next() {
+ name := iter.Label()
+ if strings.HasPrefix(name, "$") {
+ continue
+ }
+ v := iter.Value()
+
+ if err := validateEntry(name, v); err != nil {
+ return nil, err
+ }
+
+ str, ok := os.LookupEnv(name)
+ if !ok {
+ update[name] = nil
+ continue
+ }
+ x, err := fromString(name, str, v)
+ if err != nil {
+ return nil, err
+ }
+ update[name] = x
+ }
+
+ return update, nil
+}
+
+type environCmd struct{}
+
+func newEnvironCmd(v cue.Value) (task.Runner, error) {
+ return &environCmd{}, nil
+}
+
+func (c *environCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+ iter, err := v.Fields()
+ if err != nil {
+ return nil, err
+ }
+
+ update := map[string]interface{}{}
+
+ for _, kv := range os.Environ() {
+ a := strings.SplitN(kv, "=", 2)
+
+ name := a[0]
+ str := a[1]
+
+ if v := v.Lookup(name); v.Exists() {
+ update[name], err = fromString(name, str, v)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ update[name] = str
+ }
+ }
+
+ for iter.Next() {
+ name := iter.Label()
+ if strings.HasPrefix(name, "$") {
+ continue
+ }
+ if err := validateEntry(name, iter.Value()); err != nil {
+ return nil, err
+ }
+ if _, ok := update[name]; !ok {
+ update[name] = nil
+ }
+ }
+
+ return update, nil
+}
+
+func validateEntry(name string, v cue.Value) error {
+ if k := v.IncompleteKind(); k&^(cue.NumberKind|cue.NullKind|cue.BoolKind|cue.StringKind) != 0 {
+ return errors.Newf(v.Pos(),
+ "invalid type %s for environment variable %s", k, name)
+ }
+ return nil
+}
+
+func fromString(name, str string, v cue.Value) (x interface{}, err error) {
+ k := v.IncompleteKind()
+
+ var expr ast.Expr
+ var errs errors.Error
+
+ if k&cue.NumberKind != 0 {
+ expr, err = parser.ParseExpr(name, str)
+ if err != nil {
+ errs = errors.Wrapf(err, v.Pos(),
+ "invalid number for environment variable %s", name)
+ }
+ }
+
+ if k&cue.BoolKind != 0 {
+ str = strings.TrimSpace(str)
+ b, ok := boolValues[str]
+ if !ok {
+ errors.Append(errs, errors.Newf(v.Pos(),
+ "invalid boolean value %q for environment variable %s", str, name))
+ } else if expr != nil || k&cue.StringKind != 0 {
+ // Convert into an expression
+ bl := ast.NewBool(b)
+ if expr != nil {
+ expr = &ast.BinaryExpr{Op: token.OR, X: expr, Y: bl}
+ } else {
+ expr = bl
+ }
+ } else {
+ x = b
+ }
+ }
+
+ if k&cue.StringKind != 0 {
+ if expr != nil {
+ expr = &ast.BinaryExpr{Op: token.OR, X: expr, Y: ast.NewString(str)}
+ } else {
+ x = str
+ }
+ }
+
+ switch {
+ case expr != nil:
+ return expr, nil
+ case x != nil:
+ return x, nil
+ case errs == nil:
+ return nil, errors.Newf(v.Pos(),
+ "invalid type for environment variable %s", name)
+ }
+ return nil, errs
+}
+
+var boolValues = map[string]bool{
+ "1": true,
+ "0": false,
+ "t": true,
+ "f": false,
+ "T": true,
+ "F": false,
+ "true": true,
+ "false": false,
+ "TRUE": true,
+ "FALSE": false,
+ "True": true,
+ "False": false,
+}
diff --git a/pkg/tool/os/env_test.go b/pkg/tool/os/env_test.go
new file mode 100644
index 0000000..5b426b7
--- /dev/null
+++ b/pkg/tool/os/env_test.go
@@ -0,0 +1,182 @@
+// Copyright 2019 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 os
+
+import (
+ "os"
+ "testing"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal"
+ "cuelang.org/go/internal/task"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func TestSetenv(t *testing.T) {
+ os.Setenv("CUEOSTESTUNSET", "SET")
+ v := parse(t, "tool/os.Setenv", `{
+ CUEOSTESTMOOD: "yippie"
+ CUEOSTESTTRUE: true
+ CUEOSTESTFALSE: false
+ CUEOSTESTNUM: 34K
+ CUEOSTESTUNSET: null
+ }`)
+ _, err := (*setenvCmd).Run(nil, nil, v)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, p := range [][2]string{
+ {"CUEOSTESTMOOD", "yippie"},
+ {"CUEOSTESTTRUE", "1"},
+ {"CUEOSTESTFALSE", "0"},
+ {"CUEOSTESTNUM", "34000"},
+ } {
+ got := os.Getenv(p[0])
+ if got != p[1] {
+ t.Errorf("got %v; want %v", got, p[1])
+ }
+ }
+
+ if _, ok := os.LookupEnv("CUEOSTESTUNSET"); ok {
+ t.Error("CUEOSTESTUNSET should have been unset")
+ }
+
+ v = parse(t, "tool/os.Setenv", `{
+ CUEOSTESTMOOD: string
+ }`)
+ _, err = (*setenvCmd).Run(nil, nil, v)
+ if err == nil {
+ t.Fatal("expected incomplete error")
+ }
+ // XXX: ensure error is not concrete.
+}
+
+func TestGetenv(t *testing.T) {
+
+ for _, p := range [][2]string{
+ {"CUEOSTESTMOOD", "yippie"},
+ {"CUEOSTESTTRUE", "True"},
+ {"CUEOSTESTFALSE", "0"},
+ {"CUEOSTESTBI", "1"},
+ {"CUEOSTESTNUM", "34K"},
+ {"CUEOSTESTNUMD", "not a num"},
+ {"CUEOSTESTMULTI", "10"},
+ } {
+ os.Setenv(p[0], p[1])
+ }
+
+ config := `{
+ CUEOSTESTMOOD: string
+ CUEOSTESTTRUE: bool
+ CUEOSTESTFALSE: bool | string
+ CUEOSTESTBI: *bool | int,
+ CUEOSTESTNUM: int
+ CUEOSTESTNUMD: *int | *bool | string
+ CUEOSTESTMULTI: *<10 | string
+ CUEOSTESTNULL: int | null
+ }`
+
+ want := map[string]interface{}{
+ "CUEOSTESTMOOD": "yippie",
+ "CUEOSTESTTRUE": true,
+ "CUEOSTESTFALSE": &ast.BinaryExpr{
+ Op: token.OR,
+ X: ast.NewBool(false),
+ Y: ast.NewString("0"),
+ },
+ "CUEOSTESTBI": &ast.BinaryExpr{
+ Op: token.OR,
+ X: &ast.BasicLit{Kind: token.INT, Value: "1"},
+ Y: ast.NewBool(true),
+ },
+ "CUEOSTESTNUM": &ast.BasicLit{Kind: token.INT, Value: "34K"},
+ "CUEOSTESTNUMD": "not a num",
+ "CUEOSTESTMULTI": &ast.BinaryExpr{
+ Op: token.OR,
+ X: &ast.BasicLit{Kind: token.INT, Value: "10"},
+ Y: ast.NewString("10"),
+ },
+ "CUEOSTESTNULL": nil,
+ }
+
+ for _, tc := range []struct {
+ pkg string
+ runner task.Runner
+ }{
+ {"tool/os.Getenv", &getenvCmd{}},
+ {"tool/os.Environ", &environCmd{}},
+ } {
+ v := parse(t, tc.pkg, config)
+ got, err := tc.runner.Run(nil, v)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var opts = []cmp.Option{
+ cmpopts.IgnoreFields(ast.BinaryExpr{}, "OpPos"),
+ cmpopts.IgnoreFields(ast.BasicLit{}, "ValuePos"),
+ cmpopts.IgnoreUnexported(ast.BasicLit{}, ast.BinaryExpr{}),
+ // For ignoring addinonal entries from os.Environ:
+ cmpopts.IgnoreMapEntries(func(s string, x interface{}) bool {
+ _, ok := want[s]
+ return !ok
+ }),
+ }
+
+ if !cmp.Equal(got, want, opts...) {
+ t.Error(cmp.Diff(got, want, opts...))
+ }
+
+ // Errors:
+ for _, etc := range []struct{ config, err string }{{
+ config: `{ CUEOSTESTNULL: [...string] }`,
+ err: "expected unsupported type error",
+ }, {
+ config: `{ CUEOSTESTNUMD: int }`,
+ err: "expected invalid number error",
+ }, {
+ config: `{ CUEOSTESTNUMD: null }`,
+ err: "expected invalid type",
+ }} {
+ t.Run(etc.err, func(t *testing.T) {
+ v = parse(t, tc.pkg, etc.config)
+ if _, err = tc.runner.Run(nil, v); err == nil {
+ t.Error(etc.err)
+ }
+ })
+ }
+ }
+}
+
+func parse(t *testing.T, kind, expr string) cue.Value {
+ t.Helper()
+
+ x, err := parser.ParseExpr("test", expr)
+ if err != nil {
+ errors.Print(os.Stderr, err, nil)
+ t.Fatal(err)
+ }
+ var r cue.Runtime
+ i, err := r.CompileExpr(x)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return internal.UnifyBuiltin(i.Value(), kind).(cue.Value)
+}
diff --git a/pkg/tool/os/gen.go b/pkg/tool/os/gen.go
new file mode 100644
index 0000000..b6df11a
--- /dev/null
+++ b/pkg/tool/os/gen.go
@@ -0,0 +1,47 @@
+// Copyright 2019 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.
+
+// +build ignore
+
+package main
+
+// TODO: remove when we have a cuedoc server. Until then,
+// piggyback on godoc.org.
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+)
+
+const msg = `// Code generated by cue get go. DO NOT EDIT.
+
+// Package os defines tasks for retrieving os-related information.
+//
+// CUE definitions:
+// %s
+package os
+`
+
+func main() {
+ f, _ := os.Create("doc.go")
+ defer f.Close()
+ b, _ := ioutil.ReadFile("os.cue")
+ i := bytes.Index(b, []byte("package os"))
+ b = b[i+len("package os")+1:]
+ b = bytes.ReplaceAll(b, []byte("\n"), []byte("\n// "))
+ b = bytes.ReplaceAll(b, []byte("\t"), []byte(" "))
+ fmt.Fprintf(f, msg, string(b))
+}
diff --git a/pkg/tool/os/os.cue b/pkg/tool/os/os.cue
new file mode 100644
index 0000000..d7e4322
--- /dev/null
+++ b/pkg/tool/os/os.cue
@@ -0,0 +1,56 @@
+// Copyright 2019 The 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 os
+
+// A Value are all possible values allowed in flags.
+// A null value unsets an environment variable.
+Value :: bool | number | *string | null
+
+// Name indicates a valid flag name.
+Name :: !="" & !~"^[$]"
+
+// Setenv defines a set of command line flags, the values of which will be set
+// at run time. The doc comment of the flag is presented to the user in help.
+//
+// To define a shorthand, define the shorthand as a new flag referring to
+// the flag of which it is a shorthand.
+Setenv: {
+ $id: "tool/os.Setenv"
+
+ [Name]: Value
+}
+
+// Getenv gets and parses the specific command line variables.
+Getenv: {
+ $id: "tool/os.Getenv"
+
+ [Name]: Value
+}
+
+// Environ populates a struct with all environment variables.
+Environ: {
+ $id: "tool/os.Environ"
+
+ // A map of all populated values.
+ // Individual entries may be specified ahead of time to enable
+ // validation and parsing. Values that are marked as required
+ // will fail the task if they are not found.
+ [Name]: Value
+}
+
+// Clearenv clears all environment variables.
+Clearenv: {
+ $id: "tool/os.Clearenv"
+}