pkg/tool/file: support file-related tasks

Updates #39

Change-Id: I0fc36d7414b76e9a741bf3d2616653f96fc462bd
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1925
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
index 8e53f17..67f6a45 100644
--- a/cmd/cue/cmd/custom.go
+++ b/cmd/cue/cmd/custom.go
@@ -33,6 +33,7 @@
 	itask "cuelang.org/go/internal/task"
 	_ "cuelang.org/go/pkg/tool/cli" // Register tasks
 	_ "cuelang.org/go/pkg/tool/exec"
+	_ "cuelang.org/go/pkg/tool/file"
 	_ "cuelang.org/go/pkg/tool/http"
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
diff --git a/cue/builtins.go b/cue/builtins.go
index 136a1fd..f815b8e 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -1907,26 +1907,25 @@
 		native: []*builtin{{}},
 		cue: `{
 	Read: {
-		_kind:    "tool/file.Read"
+		kind:     "tool/file.Read"
 		filename: !=""
 		contents: *bytes | string
 	}
 	Create: {
-		_kind:       "tool/file.Create"
+		kind:        "tool/file.Create"
 		filename:    !=""
 		contents:    bytes | string
 		permissions: int | *420
-		overwrite:   *false | true
 	}
 	Append: {
-		_kind:       "tool/file.Append"
+		kind:        "tool/file.Append"
 		filename:    !=""
 		contents:    bytes | string
 		permissions: int | *420
 	}
 	Glob: {
-		_kind: "tool/file.Glob"
-		glob:  !=""
+		kind: "tool/file.Glob"
+		glob: !=""
 		files: [...string]
 	}
 }`,
diff --git a/pkg/tool/file/file.cue b/pkg/tool/file/file.cue
index 1275236..ebad2a5 100644
--- a/pkg/tool/file/file.cue
+++ b/pkg/tool/file/file.cue
@@ -12,66 +12,52 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package os
-
-import "tool"
+package file
 
 // Read reads the contents of a file.
-Read: tool.Task & {
-	_kind: "tool/file.Read"
+Read: {
+	kind: "tool/file.Read"
 
 	// filename names the file to read.
-	filename: string
+	filename: !=""
 
 	// contents is the read contents. If the contents are constraint to bytes
 	// (the default), the file is read as is. If it is constraint to a string,
 	// the contents are checked to be valid UTF-8.
 	contents: *bytes | string
-
-	// if body is given, the file contents are parsed as JSON and unified with
-	// the specified CUE value.
-	body?: _
-}
-
-// Create writes contents to the given file.
-Create: tool.Task & {
-	_kind: "tool/file.Create"
-
-	// filename names the file to write.
-	filename: string
-
-	// permissions defines the permissions to use if the file does not yet exist.
-	permissions: int
-
-	// overwrite defines whether an existing file may be overwritten.
-	overwrite: *false | true
-
-	// contents specifies the bytes to be written.
-	contents: bytes | string
 }
 
 // Append writes contents to the given file.
-Append: tool.Task & {
+Append: {
+	kind: "tool/file.Append"
+
 	// filename names the file to append.
-	filename: string
+	filename: !=""
 
 	// permissions defines the permissions to use if the file does not yet exist.
-	permissions: int
+	permissions: int | *0o644
 
 	// contents specifies the bytes to be written.
 	contents: bytes | string
 }
 
-Dir: tool.Task & {
-	_kind: "tool/file.Dir"
+// Create writes contents to the given file.
+Create: {
+	kind: "tool/file.Create"
 
-	path: string
-	dir: [...string]
+	// filename names the file to write.
+	filename: !=""
+
+	// permissions defines the permissions to use if the file does not yet exist.
+	permissions: int | *0o644
+
+	// contents specifies the bytes to be written.
+	contents: bytes | string
 }
 
-Glob: tool.Task & {
-	_kind: "tool/file.Glob"
+Glob: {
+	kind: "tool/file.Glob"
 
-	glob: string
-	files <Filename>: string
+	glob: !=""
+	files: [...string]
 }
diff --git a/pkg/tool/file/file.go b/pkg/tool/file/file.go
new file mode 100644
index 0000000..e5673cc
--- /dev/null
+++ b/pkg/tool/file/file.go
@@ -0,0 +1,98 @@
+// 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 file provides file operations for cue tasks.
+package file
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/internal/task"
+)
+
+func init() {
+	task.Register("tool/file.Read", newReadCmd)
+	task.Register("tool/file.Append", newAppendCmd)
+	task.Register("tool/file.Create", newCreateCmd)
+	task.Register("tool/file.Glob", newGlobCmd)
+}
+
+func newReadCmd(v cue.Value) (task.Runner, error)   { return &cmdRead{}, nil }
+func newAppendCmd(v cue.Value) (task.Runner, error) { return &cmdAppend{}, nil }
+func newCreateCmd(v cue.Value) (task.Runner, error) { return &cmdCreate{}, nil }
+func newGlobCmd(v cue.Value) (task.Runner, error)   { return &cmdGlob{}, nil }
+
+type cmdRead struct{}
+type cmdAppend struct{}
+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, v cue.Value) (res interface{}, err error) {
+	b, err := ioutil.ReadFile(lookupStr(v, "filename"))
+	if err != nil {
+		return nil, err
+	}
+	update := map[string]interface{}{"contents": b}
+
+	switch v.Lookup("contents").IncompleteKind() &^ cue.BottomKind {
+	case cue.BytesKind:
+	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")
+	mode, err := v.Lookup("permissions").Int64()
+	if err != nil {
+		return nil, err
+	}
+
+	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, os.FileMode(mode))
+	if err != nil {
+		return nil, err
+	}
+	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")
+	mode, err := v.Lookup("permissions").Int64()
+	if err != nil {
+		return nil, 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) {
+	m, err := filepath.Glob(lookupStr(v, "glob"))
+	return m, err
+}
diff --git a/pkg/tool/file/file_test.go b/pkg/tool/file/file_test.go
new file mode 100644
index 0000000..d8531b3
--- /dev/null
+++ b/pkg/tool/file/file_test.go
@@ -0,0 +1,137 @@
+// 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 file
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"reflect"
+	"testing"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
+)
+
+func parse(t *testing.T, kind, expr string) cue.Value {
+	t.Helper()
+	fset := token.NewFileSet()
+
+	x, err := parser.ParseExpr(fset, "test", expr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	i, err := cue.FromExpr(fset, x)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return internal.UnifyBuiltin(i.Value(), kind).(cue.Value)
+}
+func TestRead(t *testing.T) {
+	v := parse(t, "tool/file.Read", `{filename: "testdata/input.foo"}`)
+	got, err := (*cmdRead).Run(nil, nil, v)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := map[string]interface{}{"contents": []byte("This is a test.")}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %v; want %v", got, want)
+	}
+
+	v = parse(t, "tool/file.Read", `{
+		filename: "testdata/input.foo"
+		contents: string
+	}`)
+	got, err = (*cmdRead).Run(nil, nil, v)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want = map[string]interface{}{"contents": "This is a test."}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("got %v; want %v", got, want)
+	}
+}
+
+func TestAppend(t *testing.T) {
+	f, err := ioutil.TempFile("", "filetest")
+	if err != nil {
+		t.Fatal(err)
+	}
+	name := f.Name()
+	defer os.Remove(name)
+	f.Close()
+
+	v := parse(t, "tool/file.Append", fmt.Sprintf(`{
+		filename: "%s"
+		contents: "This is a test."
+	}`, name))
+	_, err = (*cmdAppend).Run(nil, nil, v)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	b, err := ioutil.ReadFile(name)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := string(b), "This is a test."; got != want {
+		t.Errorf("got %v; want %v", got, want)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	f, err := ioutil.TempFile("", "filetest")
+	if err != nil {
+		t.Fatal(err)
+	}
+	name := f.Name()
+	defer os.Remove(name)
+	f.Close()
+
+	v := parse(t, "tool/file.Create", fmt.Sprintf(`{
+		filename: "%s"
+		contents: "This is a test."
+	}`, name))
+	_, err = (*cmdCreate).Run(nil, nil, v)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	b, err := ioutil.ReadFile(name)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := string(b), "This is a test."; got != want {
+		t.Errorf("got %v; want %v", got, want)
+	}
+}
+
+func TestGlob(t *testing.T) {
+	v := parse(t, "tool/file.Glob", fmt.Sprintf(`{
+		glob: "testdata/input.*"
+	}`))
+	got, err := (*cmdGlob).Run(nil, nil, v)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if want := []string{"testdata/input.foo"}; !reflect.DeepEqual(got, want) {
+		t.Errorf("got %v; want %v", got, want)
+	}
+}
diff --git a/pkg/tool/file/testdata/input.foo b/pkg/tool/file/testdata/input.foo
new file mode 100644
index 0000000..273c1a9
--- /dev/null
+++ b/pkg/tool/file/testdata/input.foo
@@ -0,0 +1 @@
+This is a test.
\ No newline at end of file