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/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