pkg/tool/exec: allow passing env vars in exec.Run

Closes #260
https://github.com/cuelang/cue/pull/260

GitOrigin-RevId: 25e566f00afa109e7d6ef1762a18b0f466e819d9
Change-Id: I80de4b7d278abf87323b516e564125f582c97e9a

Closes #264
https://github.com/cuelang/cue/pull/264

GitOrigin-RevId: 097c5a03b5665e1677e91c1f64070f28787b0658
Change-Id: I937463c5fdb6b562b457fa550d2840705ebee91d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4680
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/pkg/tool/exec/doc.go b/pkg/tool/exec/doc.go
index 7a8eb45..d7cb622 100644
--- a/pkg/tool/exec/doc.go
+++ b/pkg/tool/exec/doc.go
@@ -11,12 +11,11 @@
 //     	// cmd is the command to run.
 //     	cmd: string | [string, ...string]
 //
-//     	// install is an optional command to install the binaries needed
-//     	// to run the command.
-//     	install?: string | [string, ...string]
-//
 //     	// env defines the environment variables to use for this system.
-//     	env: [string]: string
+//     	// If the value is a list, the entries mus be of the form key=value,
+//     	// where the last value takes precendence in the case of multiple
+//     	// occurrances of the same key.
+//     	env: [string]: string | [...=~"="]
 //
 //     	// stdout captures the output from stdout if it is of type bytes or string.
 //     	// The default value of null indicates it is redirected to the stdout of the
diff --git a/pkg/tool/exec/exec.cue b/pkg/tool/exec/exec.cue
index f827fd6..cd9d797 100644
--- a/pkg/tool/exec/exec.cue
+++ b/pkg/tool/exec/exec.cue
@@ -21,12 +21,11 @@
 	// cmd is the command to run.
 	cmd: string | [string, ...string]
 
-	// install is an optional command to install the binaries needed
-	// to run the command.
-	install?: string | [string, ...string]
-
 	// env defines the environment variables to use for this system.
-	env: [string]: string
+	// If the value is a list, the entries mus be of the form key=value,
+	// where the last value takes precendence in the case of multiple
+	// occurrances of the same key.
+	env: [string]: string | [...=~"="]
 
 	// stdout captures the output from stdout if it is of type bytes or string.
 	// The default value of null indicates it is redirected to the stdout of the
diff --git a/pkg/tool/exec/exec.go b/pkg/tool/exec/exec.go
index 0846b16..7c93e4b 100644
--- a/pkg/tool/exec/exec.go
+++ b/pkg/tool/exec/exec.go
@@ -42,48 +42,12 @@
 }
 
 func (c *execCmd) Run(ctx *task.Context) (res interface{}, err error) {
+	cmd, doc, err := mkCommand(ctx)
+	if err != nil {
+		return cue.Value{}, err
+	}
+
 	// TODO: set environment variables, if defined.
-	var bin string
-	var args []string
-	doc := ""
-	v := ctx.Lookup("cmd")
-	if ctx.Err != nil {
-		return nil, ctx.Err
-	}
-	switch v.Kind() {
-	case cue.StringKind:
-		str := ctx.String("cmd")
-		doc = str
-		list := strings.Fields(str)
-		bin = list[0]
-		args = append(args, list[1:]...)
-
-	case cue.ListKind:
-		list, _ := v.List()
-		if !list.Next() {
-			return cue.Value{}, errors.New("empty command list")
-		}
-		bin, err = list.Value().String()
-		if err != nil {
-			return cue.Value{}, err
-		}
-		doc += bin
-		for list.Next() {
-			str, err := list.Value().String()
-			if err != nil {
-				return cue.Value{}, err
-			}
-			args = append(args, str)
-			doc += " " + str
-		}
-	}
-
-	if bin == "" {
-		return cue.Value{}, errors.New("empty command")
-	}
-
-	cmd := exec.CommandContext(ctx.Context, bin, args...)
-
 	stream := func(name string) (stream cue.Value, ok bool) {
 		c := ctx.Obj.Lookup(name)
 		// Although the schema defines a default versions, older implementations
@@ -100,7 +64,7 @@
 	if v, ok := stream("stdin"); !ok {
 		cmd.Stdin = ctx.Stdin
 	} else if cmd.Stdin, err = v.Reader(); err != nil {
-		return nil, fmt.Errorf("cue: %v", err)
+		return nil, errors.Wrapf(err, v.Pos(), "invalid input")
 	}
 	_, captureOut := stream("stdout")
 	if !captureOut {
@@ -130,3 +94,73 @@
 	}
 	return update, err
 }
+
+func mkCommand(ctx *task.Context) (c *exec.Cmd, doc string, err error) {
+	var bin string
+	var args []string
+
+	v := ctx.Lookup("cmd")
+	if ctx.Err != nil {
+		return nil, "", ctx.Err
+	}
+
+	switch v.Kind() {
+	case cue.StringKind:
+		str := ctx.String("cmd")
+		doc = str
+		list := strings.Fields(str)
+		bin = list[0]
+		args = append(args, list[1:]...)
+
+	case cue.ListKind:
+		list, _ := v.List()
+		if !list.Next() {
+			return nil, "", errors.New("empty command list")
+		}
+		bin, err = list.Value().String()
+		if err != nil {
+			return nil, "", err
+		}
+		doc += bin
+		for list.Next() {
+			str, err := list.Value().String()
+			if err != nil {
+				return nil, "", err
+			}
+			args = append(args, str)
+			doc += " " + str
+		}
+	}
+
+	if bin == "" {
+		return nil, "", errors.New("empty command")
+	}
+
+	cmd := exec.CommandContext(ctx.Context, bin, args...)
+
+	env := ctx.Obj.Lookup("env")
+
+	// List case.
+	for iter, _ := env.List(); iter.Next(); {
+		str, err := iter.Value().String()
+		if err != nil {
+			return nil, "", errors.Wrapf(err, v.Pos(),
+				"invalid environment variable value %q", v)
+		}
+		cmd.Env = append(cmd.Env, str)
+	}
+
+	// Struct case.
+	for iter, _ := ctx.Obj.Lookup("env").Fields(); iter.Next(); {
+		label := iter.Label()
+		v := iter.Value()
+		str, err := v.String()
+		if err != nil {
+			return nil, "", errors.Wrapf(err, v.Pos(),
+				"invalid environment variable value %q", v)
+		}
+		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", label, str))
+	}
+
+	return cmd, doc, nil
+}
diff --git a/pkg/tool/exec/exec_test.go b/pkg/tool/exec/exec_test.go
new file mode 100644
index 0000000..fb50513
--- /dev/null
+++ b/pkg/tool/exec/exec_test.go
@@ -0,0 +1,71 @@
+// 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 exec
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/internal/task"
+)
+
+func TestEnv(t *testing.T) {
+	testCases := []struct {
+		desc string
+		val  string
+		env  []string
+	}{{
+		desc: "mapped",
+		val: `
+		cmd: "echo"
+		env: {
+			WHO:  "World"
+			WHAT: "Hello"
+			WHEN: "Now!"
+		}
+		`,
+		env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!" },
+	}, {
+		val: `
+		cmd: "echo"
+		env: [ "WHO=World", "WHAT=Hello", "WHEN=Now!" ]
+		`,
+		env: []string{ "WHO=World", "WHAT=Hello", "WHEN=Now!" },
+	}}
+	for _, tc := range testCases {
+		t.Run("", func(t *testing.T) {
+			var r cue.Runtime
+			inst, err := r.Compile(tc.desc, tc.val)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			cmd, _, err := mkCommand(&task.Context{
+				Context: context.Background(),
+				Obj:     inst.Value(),
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if !cmp.Equal(cmd.Env, tc.env) {
+				t.Error(cmp.Diff(cmd.Env, tc.env))
+			}
+		})
+	}
+}