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"
+}