pkg/tool: improve error reporting

Bail early in case of unavailable fields.

Adds helper methods to internal/task for this purpose.

Fixes #243

Change-Id: I664e8b91e119929be9babce293af5a31ad550dd4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4483
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
index af08fd4..ae62f5f 100644
--- a/cmd/cue/cmd/custom.go
+++ b/cmd/cue/cmd/custom.go
@@ -165,6 +165,14 @@
 }
 
 func getTasks(q []*task, v cue.Value, stack []string) ([]*task, error) {
+	// Allow non-task values, but do not allow errors.
+	if err := v.Err(); err != nil {
+		return nil, err
+	}
+	if v.Kind()&cue.StructKind == 0 {
+		return q, nil
+	}
+
 	if v.Lookup("$id").Exists() || v.Lookup("kind").Exists() {
 		t, err := newTask(len(q), stack, v)
 		if err != nil {
@@ -289,7 +297,11 @@
 			// NOTE: ignore the linter warning for the following line:
 			// itask.Context is an internal type and we want to break if any
 			// fields are added.
-			update, err := t.Run(&itask.Context{ctx, stdout, stderr}, obj)
+			c := &itask.Context{ctx, stdout, stderr, obj, nil}
+			update, err := t.Run(c)
+			if c.Err != nil {
+				err = c.Err
+			}
 			if err == nil && update != nil {
 				cr.root, err = cr.root.Fill(update, cr.taskPath(t)...)
 
@@ -448,6 +460,6 @@
 
 type testServerCmd string
 
-func (s testServerCmd) Run(ctx *itask.Context, v cue.Value) (x interface{}, err error) {
+func (s testServerCmd) Run(ctx *itask.Context) (x interface{}, err error) {
 	return map[string]interface{}{"url": string(s)}, nil
 }
diff --git a/cmd/cue/cmd/testdata/script/cmd_err.txt b/cmd/cue/cmd/testdata/script/cmd_err.txt
new file mode 100644
index 0000000..61b9b42
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/cmd_err.txt
@@ -0,0 +1,19 @@
+! cue cmd ref
+! stdout .
+cmp stderr cmd_badfields.out
+
+-- cmd_badfields.out --
+command.ref.task.display.filename: non-concrete value string:
+    tool/file:9:16
+command.ref.task.display.contents: invalid bytes argument for field "contents": non-concrete value string|bytes:
+    ./task_tool.cue:6:17
+-- task_tool.cue --
+package home
+
+import "tool/file"
+
+command: ref: {
+	task: display: file.Create & {
+        filename: filename
+	}
+}
diff --git a/internal/task/task.go b/internal/task/task.go
index b494a1a..a6e563c 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -21,6 +21,7 @@
 	"sync"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/errors"
 )
 
 // A Context provides context for running a task.
@@ -28,6 +29,61 @@
 	Context context.Context
 	Stdout  io.Writer
 	Stderr  io.Writer
+	Obj     cue.Value
+	Err     errors.Error
+}
+
+func (c *Context) Lookup(field string) cue.Value {
+	f := c.Obj.Lookup(field)
+	if !f.Exists() {
+		c.Err = errors.Append(c.Err, errors.Newf(c.Obj.Pos(),
+			"could not find field %q", field))
+		return cue.Value{}
+	}
+	if err := f.Err(); err != nil {
+		c.Err = errors.Append(c.Err, errors.Promote(err, "lookup"))
+	}
+	return f
+}
+
+func (c *Context) Int64(field string) int64 {
+	f := c.Obj.Lookup(field)
+	value, err := f.Int64()
+	if err != nil {
+		// TODO: use v for position for now, as f has not yet a
+		// position associated with it.
+		c.Err = errors.Append(c.Err, errors.Wrapf(err, c.Obj.Pos(),
+			"invalid integer argument for field %q", field))
+		return 0
+	}
+	return value
+}
+
+func (c *Context) String(field string) string {
+	f := c.Obj.Lookup(field)
+	value, err := f.String()
+	if err != nil {
+		// TODO: use v for position for now, as f has not yet a
+		// position associated with it.
+		c.Err = errors.Append(c.Err, errors.Wrapf(err, c.Obj.Pos(),
+			"invalid string argument for field %q", field))
+		c.Err = errors.Promote(err, "ddd")
+		return ""
+	}
+	return value
+}
+
+func (c *Context) Bytes(field string) []byte {
+	f := c.Obj.Lookup(field)
+	value, err := f.Bytes()
+	if err != nil {
+		// TODO: use v for position for now, as f has not yet a
+		// position associated with it.
+		c.Err = errors.Append(c.Err, errors.Wrapf(err, c.Obj.Pos(),
+			"invalid bytes argument for field %q", field))
+		return nil
+	}
+	return value
 }
 
 // A RunnerFunc creates a Runner.
@@ -42,7 +98,7 @@
 
 	// Runner runs given the current value and returns a new value which is to
 	// be unified with the original result.
-	Run(ctx *Context, v cue.Value) (results interface{}, err error)
+	Run(ctx *Context) (results interface{}, err error)
 }
 
 // Register registers a task for cue commands.
diff --git a/pkg/tool/cli/cli.go b/pkg/tool/cli/cli.go
index 4b41c09..dd29681 100644
--- a/pkg/tool/cli/cli.go
+++ b/pkg/tool/cli/cli.go
@@ -36,10 +36,10 @@
 	return &printCmd{}, nil
 }
 
-func (c *printCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	str, err := v.Lookup("text").String()
-	if err != nil {
-		return nil, err
+func (c *printCmd) Run(ctx *task.Context) (res interface{}, err error) {
+	str := ctx.String("text")
+	if ctx.Err != nil {
+		return nil, ctx.Err
 	}
 	fmt.Fprintln(ctx.Stdout, str)
 	return nil, nil
diff --git a/pkg/tool/exec/exec.go b/pkg/tool/exec/exec.go
index 6146e53..c64dc1c 100644
--- a/pkg/tool/exec/exec.go
+++ b/pkg/tool/exec/exec.go
@@ -40,17 +40,18 @@
 	return &execCmd{}, nil
 }
 
-func (c *execCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+func (c *execCmd) Run(ctx *task.Context) (res interface{}, err error) {
 	// TODO: set environment variables, if defined.
 	var bin string
 	var args []string
 	doc := ""
-	switch v := v.Lookup("cmd"); v.Kind() {
+	v := ctx.Lookup("cmd")
+	if ctx.Err != nil {
+		return nil, ctx.Err
+	}
+	switch v.Kind() {
 	case cue.StringKind:
-		str, _ := v.String()
-		if str == "" {
-			return cue.Value{}, errors.New("empty command")
-		}
+		str := ctx.String("cmd")
 		doc = str
 		list := strings.Fields(str)
 		bin = list[0]
@@ -76,16 +77,20 @@
 		}
 	}
 
+	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 := v.Lookup(name)
+		c := ctx.Obj.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 {
+		if err := c.Null(); ctx.Err != nil || err == nil {
 			return
 		}
 		return c, true
diff --git a/pkg/tool/file/file.go b/pkg/tool/file/file.go
index 694f832..b0d1e23 100644
--- a/pkg/tool/file/file.go
+++ b/pkg/tool/file/file.go
@@ -42,32 +42,35 @@
 type cmdCreate struct{}
 type cmdGlob struct{}
 
-func lookupStr(v cue.Value, str string) string {
-	str, _ = v.Lookup(str).String()
-	return str
-}
+func (c *cmdRead) Run(ctx *task.Context) (res interface{}, err error) {
+	filename := ctx.String("filename")
+	if ctx.Err != nil {
+		return nil, ctx.Err
+	}
 
-func (c *cmdRead) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	b, err := ioutil.ReadFile(lookupStr(v, "filename"))
+	b, err := ioutil.ReadFile(filename)
 	if err != nil {
 		return nil, err
 	}
 	update := map[string]interface{}{"contents": b}
 
-	switch v.Lookup("contents").IncompleteKind() {
+	switch v := ctx.Lookup("contents"); v.IncompleteKind() {
 	case cue.BytesKind:
+		// already set above
 	case cue.StringKind:
 		update["contents"] = string(b)
 	}
 	return update, nil
 }
 
-func (c *cmdAppend) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	filename := lookupStr(v, "filename")
-	filename = filepath.FromSlash(filename)
-	mode, err := v.Lookup("permissions").Int64()
-	if err != nil {
-		return nil, err
+func (c *cmdAppend) Run(ctx *task.Context) (res interface{}, err error) {
+	var (
+		filename = filepath.FromSlash(ctx.String("filename"))
+		mode     = ctx.Int64("permissions")
+		b        = ctx.Bytes("contents")
+	)
+	if ctx.Err != nil {
+		return nil, ctx.Err
 	}
 
 	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, os.FileMode(mode))
@@ -76,27 +79,30 @@
 	}
 	defer f.Close()
 
-	b, _ := v.Lookup("contents").Bytes()
 	if _, err := f.Write(b); err != nil {
 		return nil, err
 	}
 	return nil, nil
 }
 
-func (c *cmdCreate) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	filename := lookupStr(v, "filename")
-	filename = filepath.FromSlash(filename)
-	mode, err := v.Lookup("permissions").Int64()
-	if err != nil {
-		return nil, err
+func (c *cmdCreate) Run(ctx *task.Context) (res interface{}, err error) {
+	var (
+		filename = filepath.FromSlash(ctx.String("filename"))
+		mode     = ctx.Int64("permissions")
+		b        = ctx.Bytes("contents")
+	)
+	if ctx.Err != nil {
+		return nil, ctx.Err
 	}
 
-	b, _ := v.Lookup("contents").Bytes()
 	return nil, ioutil.WriteFile(filename, b, os.FileMode(mode))
 }
 
-func (c *cmdGlob) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	glob := filepath.FromSlash(lookupStr(v, "glob"))
+func (c *cmdGlob) Run(ctx *task.Context) (res interface{}, err error) {
+	glob := ctx.String("glob")
+	if ctx.Err != nil {
+		return nil, ctx.Err
+	}
 	m, err := filepath.Glob(glob)
 	for i, s := range m {
 		m[i] = filepath.ToSlash(s)
diff --git a/pkg/tool/file/file_test.go b/pkg/tool/file/file_test.go
index b284445..6dfb17f 100644
--- a/pkg/tool/file/file_test.go
+++ b/pkg/tool/file/file_test.go
@@ -25,6 +25,7 @@
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/internal"
+	"cuelang.org/go/internal/task"
 )
 
 func parse(t *testing.T, kind, expr string) cue.Value {
@@ -43,7 +44,7 @@
 }
 func TestRead(t *testing.T) {
 	v := parse(t, "tool/file.Read", `{filename: "testdata/input.foo"}`)
-	got, err := (*cmdRead).Run(nil, nil, v)
+	got, err := (*cmdRead).Run(nil, &task.Context{Obj: v})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -56,7 +57,7 @@
 		filename: "testdata/input.foo"
 		contents: string
 	}`)
-	got, err = (*cmdRead).Run(nil, nil, v)
+	got, err = (*cmdRead).Run(nil, &task.Context{Obj: v})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -80,7 +81,7 @@
 		filename: "%s"
 		contents: "This is a test."
 	}`, name))
-	_, err = (*cmdAppend).Run(nil, nil, v)
+	_, err = (*cmdAppend).Run(nil, &task.Context{Obj: v})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -109,7 +110,7 @@
 		filename: "%s"
 		contents: "This is a test."
 	}`, name))
-	_, err = (*cmdCreate).Run(nil, nil, v)
+	_, err = (*cmdCreate).Run(nil, &task.Context{Obj: v})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -128,7 +129,7 @@
 	v := parse(t, "tool/file.Glob", fmt.Sprintf(`{
 		glob: "testdata/input.*"
 	}`))
-	got, err := (*cmdGlob).Run(nil, nil, v)
+	got, err := (*cmdGlob).Run(nil, &task.Context{Obj: v})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/tool/http/http.go b/pkg/tool/http/http.go
index f43ff7c..eaa77bc 100644
--- a/pkg/tool/http/http.go
+++ b/pkg/tool/http/http.go
@@ -38,20 +38,14 @@
 	return &httpCmd{}, nil
 }
 
-func lookupString(obj cue.Value, key string) string {
-	str, err := obj.Lookup(key).String()
-	if err != nil {
-		return ""
-	}
-	return str
-}
-
-func (c *httpCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+func (c *httpCmd) Run(ctx *task.Context) (res interface{}, err error) {
 	var header, trailer http.Header
-	method := lookupString(v, "method")
-	u := lookupString(v, "url")
+	var (
+		method = ctx.String("method")
+		u      = ctx.String("url")
+	)
 	var r io.Reader
-	if obj := v.Lookup("request"); v.Exists() {
+	if obj := ctx.Obj.Lookup("request"); obj.Exists() {
 		if v := obj.Lookup("body"); v.Exists() {
 			r, err = v.Reader()
 			if err != nil {
@@ -65,6 +59,10 @@
 			return nil, err
 		}
 	}
+	if ctx.Err != nil {
+		return nil, ctx.Err
+	}
+
 	req, err := http.NewRequest(method, u, r)
 	if err != nil {
 		return nil, err
diff --git a/pkg/tool/os/env.go b/pkg/tool/os/env.go
index 487d397..32880ff 100644
--- a/pkg/tool/os/env.go
+++ b/pkg/tool/os/env.go
@@ -51,7 +51,7 @@
 	return &clearenvCmd{}, nil
 }
 
-func (c *clearenvCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+func (c *clearenvCmd) Run(ctx *task.Context) (res interface{}, err error) {
 	os.Clearenv()
 	return map[string]interface{}{}, nil
 }
@@ -62,8 +62,8 @@
 	return &setenvCmd{}, nil
 }
 
-func (c *setenvCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	iter, err := v.Fields()
+func (c *setenvCmd) Run(ctx *task.Context) (res interface{}, err error) {
+	iter, err := ctx.Obj.Fields()
 	if err != nil {
 		return nil, err
 	}
@@ -117,8 +117,8 @@
 	return &getenvCmd{}, nil
 }
 
-func (c *getenvCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	iter, err := v.Fields()
+func (c *getenvCmd) Run(ctx *task.Context) (res interface{}, err error) {
+	iter, err := ctx.Obj.Fields()
 	if err != nil {
 		return nil, err
 	}
@@ -157,8 +157,8 @@
 	return &environCmd{}, nil
 }
 
-func (c *environCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
-	iter, err := v.Fields()
+func (c *environCmd) Run(ctx *task.Context) (res interface{}, err error) {
+	iter, err := ctx.Obj.Fields()
 	if err != nil {
 		return nil, err
 	}
@@ -171,7 +171,7 @@
 		name := a[0]
 		str := a[1]
 
-		if v := v.Lookup(name); v.Exists() {
+		if v := ctx.Obj.Lookup(name); v.Exists() {
 			update[name], err = fromString(name, str, v)
 			if err != nil {
 				return nil, err
diff --git a/pkg/tool/os/env_test.go b/pkg/tool/os/env_test.go
index 5b426b7..8b5adef 100644
--- a/pkg/tool/os/env_test.go
+++ b/pkg/tool/os/env_test.go
@@ -38,7 +38,7 @@
 		CUEOSTESTNUM:   34K
 		CUEOSTESTUNSET: null
 	}`)
-	_, err := (*setenvCmd).Run(nil, nil, v)
+	_, err := (*setenvCmd).Run(nil, &task.Context{Obj: v})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -61,7 +61,7 @@
 	v = parse(t, "tool/os.Setenv", `{
 		CUEOSTESTMOOD: string
 	}`)
-	_, err = (*setenvCmd).Run(nil, nil, v)
+	_, err = (*setenvCmd).Run(nil, &task.Context{Obj: v})
 	if err == nil {
 		t.Fatal("expected incomplete error")
 	}
@@ -124,7 +124,7 @@
 		{"tool/os.Environ", &environCmd{}},
 	} {
 		v := parse(t, tc.pkg, config)
-		got, err := tc.runner.Run(nil, v)
+		got, err := tc.runner.Run(&task.Context{Obj: v})
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -157,7 +157,7 @@
 		}} {
 			t.Run(etc.err, func(t *testing.T) {
 				v = parse(t, tc.pkg, etc.config)
-				if _, err = tc.runner.Run(nil, v); err == nil {
+				if _, err = tc.runner.Run(&task.Context{Obj: v}); err == nil {
 					t.Error(etc.err)
 				}
 			})