pkg/tool/exec: fix null bug for stderr and stdout

Also slightly changed the definition. It occured to me
that specifying null to redirect to stdout is slightly
odd. It now discards output if null is specified and
redirects to stdout the field is unspecified.

Change-Id: I9204cfd98fdb81a247a07cadc1ca6f9b4724eaca
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3400
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
index 780db31..2d64bfb 100644
--- a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
+++ b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
@@ -1,3 +1,3 @@
 text: conflicting values 42 and string (mismatched types int and string):
-    ./testdata/tasks/task_tool.cue:29:9
+    ./testdata/tasks/task_tool.cue:31:9
     tool/cli:4:9
diff --git a/cmd/cue/cmd/testdata/tasks/task_tool.cue b/cmd/cue/cmd/testdata/tasks/task_tool.cue
index 0c6c737..228222b 100644
--- a/cmd/cue/cmd/testdata/tasks/task_tool.cue
+++ b/cmd/cue/cmd/testdata/tasks/task_tool.cue
@@ -1,5 +1,7 @@
 package home
 
+import "tool/exec"
+
 command run: runBase & {
 	task echo cmd: "echo \(message)"
 }
@@ -9,7 +11,7 @@
 }
 
 command errcode: {
-	task bad: {
+	task bad: exec.Run & {
 		kind:   "exec"
 		cmd:    "ls --badflags"
 		stderr: string // suppress error message
@@ -17,7 +19,7 @@
 
 // TODO: capture stdout and stderr for tests.
 command runRedirect: {
-	task echo: {
+	task echo: exec.Run & {
 		kind: "exec"
 		cmd:  "echo \(message)"
 	}
diff --git a/cue/builtins.go b/cue/builtins.go
index 7cbd04f..a055e9a 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -2871,7 +2871,7 @@
 		}
 		stdout:  *null | string | bytes
 		stderr:  *null | string | bytes
-		stdin?:  string | bytes
+		stdin:   *null | string | bytes
 		success: bool
 	}
 }`,
diff --git a/pkg/tool/exec/exec.cue b/pkg/tool/exec/exec.cue
index 78efc4e..ede6634 100644
--- a/pkg/tool/exec/exec.cue
+++ b/pkg/tool/exec/exec.cue
@@ -36,8 +36,9 @@
 	// stderr is like stdout, but for errors.
 	stderr: *null | string | bytes
 
-	// stdin specifies the input for the process.
-	stdin?: string | bytes
+	// stdin specifies the input for the process. If null, stdin of the current
+	// process is used.
+	stdin: *null | string | bytes
 
 	// success is set to true when the process terminates with with a zero exit
 	// code or false otherwise. The user can explicitly specify the value
diff --git a/pkg/tool/exec/exec.go b/pkg/tool/exec/exec.go
index f4b19b2..1bdca9a 100644
--- a/pkg/tool/exec/exec.go
+++ b/pkg/tool/exec/exec.go
@@ -78,16 +78,29 @@
 
 	cmd := exec.CommandContext(ctx.Context, bin, args...)
 
-	if v := v.Lookup("stdin"); v.Exists() {
+	stream := func(name string) (stream cue.Value, ok bool) {
+		c := v.Lookup(name)
+		// Although the schema defines a default versions, older implementations
+		// may not use it yet.
+		if !c.Exists() {
+			return
+		}
+		if err := c.Null(); err == nil {
+			return
+		}
+		return c, true
+	}
+
+	if v, ok := stream("stdin"); ok {
 		if cmd.Stdin, err = v.Reader(); err != nil {
 			return nil, fmt.Errorf("cue: %v", err)
 		}
 	}
-	captureOut := v.Lookup("stdout").Exists()
+	_, captureOut := stream("stdout")
 	if !captureOut {
 		cmd.Stdout = ctx.Stdout
 	}
-	captureErr := v.Lookup("stderr").Exists()
+	_, captureErr := stream("stderr")
 	if !captureErr {
 		cmd.Stderr = ctx.Stderr
 	}