cmd/cue: first implementation of cue tool
Change-Id: I9f6882d60c1f0dc6e4184fb8883f86ed0cacfa6e
diff --git a/cmd/cue/cmd/cmd.go b/cmd/cue/cmd/cmd.go
new file mode 100644
index 0000000..b1b7f6a
--- /dev/null
+++ b/cmd/cue/cmd/cmd.go
@@ -0,0 +1,220 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// TODO: generate long description from documentation.
+
+// cmdCmd represents the cmd command
+var cmdCmd = &cobra.Command{
+ Use: "cmd <name> [-x] [instances]",
+ Short: "run a user-defined shell command",
+ Long: `cmd executes defined the named command for each of the named instances.
+
+Commands define actions on instances. For example, they may specify
+how to upload a configuration to Kubernetes. Commands are defined
+directly in tool files, which are regular CUE files within the same
+package with a filename ending in _tool.cue. These are typically
+defined at the top of the module root so that they apply to all
+instances.
+
+Each command consists of one or more tasks. A task may load or write
+a file, consult a user on the command line, fetch a web page, and
+so on. Each task has inputs and outputs. Outputs are typically are
+filled out by the task implementation as the task completes.
+
+Inputs of tasks my refer to outputs of other tasks. The cue tool does
+a static analysis of the configuration and only starts tasks that are
+fully specified. Upon completion of each task, cue rewrites the instance,
+filling in the completed task, and reevaluates which other tasks can
+now start, and so on until all tasks have completed.
+
+Commands are defined at the top-level of the configuration:
+
+ command <Name>: { // from tool.Command
+ // usage gives a short usage pattern of the command.
+ // Example:
+ // fmt [-n] [-x] [packages]
+ usage: Name | string
+
+ // short gives a brief on-line description of the command.
+ // Example:
+ // reformat package sources
+ short: "" | string
+
+ // long gives a detailed description of the command, including a
+ // description of flags usage and examples.
+ long: "" | string
+
+ // A task defines a single action to be run as part of this command.
+ // Each task can have inputs and outputs, depending on the type
+ // task. The outputs are initially unspecified, but are filled out
+ // by the tooling
+ task <Name>: { // from "tool".Task
+ // supported fields depend on type
+ }
+
+ VarValue = string | bool | int | float | [...string|int|float]
+
+ // var declares values that can be set by command line flags or
+ // environment variables.
+ //
+ // Example:
+ // // environment to run in
+ // var env: "test" | "prod"
+ // The tool would print documentation of this flag as:
+ // Flags:
+ // --env string environment to run in: test(default) or prod
+ var <Name>: VarValue
+
+ // flag defines a command line flag.
+ //
+ // Example:
+ // var env: "test" | "prod"
+ //
+ // // augment the flag information for var
+ // flag env: {
+ // shortFlag: "e"
+ // description: "environment to run in"
+ // }
+ //
+ // The tool would print documentation of this flag as:
+ // Flags:
+ // -e, --env string environment to run in: test(default), staging, or prod
+ //
+ flag <Name>: { // from "tool".Flag
+ // value defines the possible values for this flag.
+ // The default is string. Users can define default values by
+ // using disjunctions.
+ value: env[Name].value | VarValue
+
+ // name, if set, allows var to be set with the command-line flag
+ // of the given name. null disables the command line flag.
+ name: Name | null | string
+
+ // short defines an abbreviated version of the flag.
+ // Disabled by default.
+ short: null | string
+ }
+
+ // populate flag with the default values for
+ flag: { "\(k)": { value: v } | null for k, v in var }
+
+ // env defines environment variables. It is populated with values
+ // for var.
+ //
+ // To specify a var without an equivalent environment variable,
+ // either specify it as a flag directly or disable the equally
+ // named env entry explicitly:
+ //
+ // var foo: string
+ // env foo: null // don't use environment variables for foo
+ //
+ env <Name>: {
+ // name defines the environment variable that sets this flag.
+ name: "CUE_VAR_" + strings.Upper(Name) | string | null
+
+ // The value retrieved from the environment variable or null
+ // if not set.
+ value: null | string | bytes
+ }
+ env: { "\(k)": { value: v } | null for k, v in var }
+ }
+
+Available tasks can be found in the package documentation at
+
+ cuelang.org/pkg/tool.
+
+More on tasks can be found in the tasks topic.
+
+Examples:
+
+A simple file using command line execution:
+
+ $ cat <<EOF > hello_tool.cue
+ package foo
+
+ import "tool/exec"
+
+ city: "Amsterdam"
+
+ // Say hello!
+ command hello: {
+ // whom to say hello to
+ var who: "World" | string
+
+ task print: exec.Run({
+ cmd: "echo Hello \(var.who)! Welcome to \(city)."
+ })
+ }
+ EOF
+
+ $ cue cmd echo
+ Hello World! Welcome to Amsterdam.
+
+ $ cue cmd echo -who you
+ Hello you! Welcome to Amsterdam.
+
+
+An example using pipes:
+
+ package foo
+
+ import "tool/exec"
+
+ city: "Amsterdam"
+
+ // Say hello!
+ command hello: {
+ var file: "out.txt" | string // save transcript to this file
+
+ task ask: cli.Ask({
+ prompt: "What is your name?"
+ response: string
+ })
+
+ // starts after ask
+ task echo: exec.Run({
+ cmd: ["echo", "Hello", task.ask.response + "!"]
+ stdout: string // capture stdout
+ })
+
+ // starts after echo
+ task write: file.Append({
+ filename: var.file
+ contents: task.echo.stdout
+ })
+
+ // also starts after echo
+ task print: cli.Print({
+ contents: task.echo.stdout
+ })
+ }
+
+`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ fmt.Println("cmd run but shouldn't")
+ return nil
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(cmdCmd)
+}
diff --git a/cmd/cue/cmd/cmd_test.go b/cmd/cue/cmd/cmd_test.go
new file mode 100644
index 0000000..514a49d
--- /dev/null
+++ b/cmd/cue/cmd/cmd_test.go
@@ -0,0 +1,50 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "testing"
+
+ "cuelang.org/go/cue/errors"
+ "github.com/spf13/cobra"
+)
+
+func TestCmd(t *testing.T) {
+ testCases := []string{
+ "echo",
+ "run",
+ "run_list",
+ "baddisplay",
+ "http",
+ }
+ for _, name := range testCases {
+ run := func(cmd *cobra.Command, args []string) error {
+ testOut = cmd.OutOrStdout()
+ defer func() { testOut = nil }()
+
+ tools := buildTools(RootCmd, args)
+ cmd, err := addCustom(RootCmd, "command", name, tools)
+ if err != nil {
+ return err
+ }
+ err = executeTasks("command", name, tools)
+ if err != nil {
+ errors.Print(testOut, err)
+ }
+ return nil
+ }
+ runCommand(t, run, "cmd_"+name)
+ }
+}
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
new file mode 100644
index 0000000..1f93ab9
--- /dev/null
+++ b/cmd/cue/cmd/common.go
@@ -0,0 +1,86 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/build"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/load"
+ "github.com/spf13/cobra"
+)
+
+func exitIfErr(cmd *cobra.Command, err error) {
+ if err != nil {
+ errors.Print(cmd.OutOrStderr(), err)
+ exit()
+ }
+}
+
+func buildFromArgs(cmd *cobra.Command, args []string) []*cue.Instance {
+ binst := loadFromArgs(cmd, args)
+ if binst == nil {
+ return nil
+ }
+ return buildInstances(cmd, binst)
+}
+
+func loadFromArgs(cmd *cobra.Command, args []string) []*build.Instance {
+ log.SetOutput(cmd.OutOrStderr())
+ binst := load.Instances(args, nil)
+ if len(binst) == 0 {
+ return nil
+ }
+ return binst
+}
+
+func buildInstances(cmd *cobra.Command, binst []*build.Instance) []*cue.Instance {
+ instances := cue.Build(binst)
+ for _, inst := range instances {
+ // TODO: consider merging errors of multiple files, but ensure
+ // duplicates are removed.
+ exitIfErr(cmd, inst.Err)
+ }
+
+ for _, inst := range instances {
+ // TODO: consider merging errors of multiple files, but ensure
+ // duplicates are removed.
+ exitIfErr(cmd, inst.Value().Validate())
+ }
+ return instances
+}
+
+func buildTools(cmd *cobra.Command, args []string) *cue.Instance {
+ binst := loadFromArgs(cmd, args)
+ if len(binst) == 0 {
+ return nil
+ }
+
+ included := map[string]bool{}
+
+ ti := binst[0].Context().NewInstance(binst[0].Root, nil)
+ for _, inst := range binst {
+ for _, f := range inst.ToolCUEFiles {
+ if file := inst.Abs(f); !included[file] {
+ ti.AddFile(file, nil)
+ included[file] = true
+ }
+ }
+ }
+
+ inst := cue.Merge(buildInstances(cmd, binst)...).Build(ti)
+ exitIfErr(cmd, inst.Err)
+ return inst
+}
diff --git a/cmd/cue/cmd/common_test.go b/cmd/cue/cmd/common_test.go
new file mode 100644
index 0000000..fe71588
--- /dev/null
+++ b/cmd/cue/cmd/common_test.go
@@ -0,0 +1,109 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+
+ "cuelang.org/go/cue/errors"
+ "github.com/spf13/cobra"
+ "golang.org/x/sync/errgroup"
+)
+
+var _ = errors.Print
+
+var update = flag.Bool("update", false, "update the test files")
+
+func runCommand(t *testing.T, f func(cmd *cobra.Command, args []string) error, name string, args ...string) {
+ t.Helper()
+ log.SetFlags(0)
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ const dir = "./testdata"
+ filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ t.Run(path, func(t *testing.T) {
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !info.IsDir() || dir == path {
+ return
+ }
+ testfile := filepath.Join(path, name+".out")
+ bWant, err := ioutil.ReadFile(testfile)
+ if err != nil {
+ // Don't write the file if it doesn't exist, even in *update
+ // mode. We don't want to need to support all commands for all
+ // directories. Touch the file and use *update to create it.
+ return
+ }
+
+ cmd := &cobra.Command{RunE: f}
+ cmd.SetArgs(append(args, "./"+path))
+ rOut, wOut := io.Pipe()
+ cmd.SetOutput(wOut)
+ var bOut []byte
+ g := errgroup.Group{}
+ g.Go(func() error {
+ defer wOut.Close()
+ defer func() {
+ if e := recover(); e != nil {
+ if err, ok := e.(error); ok {
+ errors.Print(wOut, err)
+ } else {
+ fmt.Fprintln(wOut, e)
+ }
+ }
+ }()
+ cmd.Execute()
+ return nil
+ })
+ g.Go(func() error {
+ bOut, err = ioutil.ReadAll(rOut)
+ return err
+ })
+ if err := g.Wait(); err != nil {
+ t.Error(err)
+ }
+ bOut = bytes.Replace(bOut, []byte(cwd), []byte("$CWD"), -1)
+ re := regexp.MustCompile("/.*/cue/")
+ bOut = re.ReplaceAll(bOut, []byte(`$$HOME/cue/`))
+ if *update {
+ ioutil.WriteFile(testfile, bOut, 0644)
+ return
+ }
+ got, want := string(bOut), string(bWant)
+ if got != want {
+ t.Errorf("\n got: %v\nwant: %v", got, want)
+ }
+ })
+ return nil
+ })
+}
+
+func TestLoadError(t *testing.T) {
+ runCommand(t, evalCmd.RunE, "loaderr", "non-existing", ".")
+}
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
new file mode 100644
index 0000000..fd7ead9
--- /dev/null
+++ b/cmd/cue/cmd/custom.go
@@ -0,0 +1,490 @@
+// Copyright 2018 The 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 cmd
+
+// This file contains code or initializing and running custom commands.
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/exec"
+ "strings"
+ "sync"
+
+ "cuelang.org/go/cue"
+ "github.com/spf13/cobra"
+ "golang.org/x/sync/errgroup"
+)
+
+const (
+ commandSection = "command"
+ taskSection = "task"
+)
+
+func lookupString(obj cue.Value, key string) string {
+ str, err := obj.Lookup(key).String()
+ if err != nil {
+ return ""
+ }
+ return str
+}
+
+func addCustom(parent *cobra.Command, typ, name string, tools *cue.Instance) (*cobra.Command, error) {
+ if tools == nil {
+ return nil, errors.New("no commands defined")
+ }
+
+ // TODO: validate allowing incomplete.
+ o := tools.Lookup(typ, name)
+ if !o.Exists() {
+ return nil, o.Err()
+ }
+
+ usage := lookupString(o, "usage")
+ if usage == "" {
+ usage = name
+ }
+ sub := &cobra.Command{
+ Use: usage,
+ Short: lookupString(o, "short"),
+ Long: lookupString(o, "long"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // - parse flags and env vars
+ // - constrain current config with config section
+
+ return doTasks(typ, name, tools)
+ },
+ }
+ parent.AddCommand(sub)
+
+ // TODO: implement var/flag handling.
+ return sub, nil
+}
+
+type taskKey struct {
+ typ string
+ name string
+ task string
+}
+
+func (k taskKey) keyForTask(taskName string) taskKey {
+ k.task = taskName
+ return k
+}
+
+func keyForReference(ref []string) (k taskKey) {
+ // command <command> task <task>
+ if len(ref) >= 4 && ref[2] == taskSection {
+ k.typ = ref[0]
+ k.name = ref[1]
+ k.task = ref[3]
+ }
+ return k
+}
+
+func (k taskKey) taskPath(task string) []string {
+ k.task = task
+ return []string{k.typ, k.name, taskSection, task}
+}
+
+func (k *taskKey) lookupTasks(root *cue.Instance) cue.Value {
+ return root.Lookup(k.typ, k.name, taskSection)
+}
+
+func doTasks(typ, command string, root *cue.Instance) error {
+ if err := executeTasks(typ, command, root); err != nil {
+ return fmt.Errorf("failed to run instance %q: %v", root.Dir, err)
+ }
+ return nil
+}
+
+// executeTasks runs user-defined tasks as part of a user-defined command.
+//
+// All tasks are started at once, but will block until tasks that they depend
+// on will continue.
+func executeTasks(typ, command string, root *cue.Instance) error {
+ spec := taskKey{typ, command, ""}
+ tasks := spec.lookupTasks(root)
+
+ index := map[taskKey]*task{}
+
+ // Create task entries from spec.
+ queue := []*task{}
+ iter, err := tasks.Fields()
+ if err != nil {
+ return err
+ }
+ for i := 0; iter.Next(); i++ {
+ t, err := newTask(i, iter.Label(), iter.Value())
+ if err != nil {
+ return err
+ }
+ queue = append(queue, t)
+ index[spec.keyForTask(iter.Label())] = t
+ }
+
+ // Mark dependencies for unresolved nodes.
+ for _, t := range queue {
+ tasks.Lookup(t.name).Walk(func(v cue.Value) bool {
+ // if v.IsIncomplete() {
+ for _, r := range v.References() {
+ if dep, ok := index[keyForReference(r)]; ok {
+ v := root.Lookup(r...)
+ if v.IsIncomplete() && v.Kind() != cue.StructKind {
+ t.dep[dep] = true
+ }
+ }
+ }
+ // }
+ return true
+ }, nil)
+ }
+
+ if isCyclic(queue) {
+ return errors.New("cyclic dependency in tasks") // TODO: better message.
+ }
+
+ ctx := context.Background()
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ var m sync.Mutex
+
+ g, ctx := errgroup.WithContext(ctx)
+ for _, t := range queue {
+ t := t
+ g.Go(func() error {
+ for d := range t.dep {
+ <-d.done
+ }
+ defer close(t.done)
+ m.Lock()
+ obj := tasks.Lookup(t.name)
+ m.Unlock()
+ update, err := t.Run(ctx, obj)
+ if err == nil && update != nil {
+ m.Lock()
+ root, err = root.Fill(update, spec.taskPath(t.name)...)
+
+ if err == nil {
+ tasks = spec.lookupTasks(root)
+ }
+ m.Unlock()
+ }
+ if err != nil {
+ cancel()
+ }
+ return err
+ })
+ }
+ return g.Wait()
+}
+
+func isCyclic(tasks []*task) bool {
+ cc := cycleChecker{
+ visited: make([]bool, len(tasks)),
+ stack: make([]bool, len(tasks)),
+ }
+ for _, t := range tasks {
+ if cc.isCyclic(t) {
+ return true
+ }
+ }
+ return false
+}
+
+type cycleChecker struct {
+ visited, stack []bool
+}
+
+func (cc *cycleChecker) isCyclic(t *task) bool {
+ i := t.index
+ if !cc.visited[i] {
+ cc.visited[i] = true
+ cc.stack[i] = true
+
+ for d := range t.dep {
+ if !cc.visited[d.index] && cc.isCyclic(d) {
+ return true
+ } else if cc.stack[d.index] {
+ return true
+ }
+ }
+ }
+ cc.stack[i] = false
+ return false
+}
+
+type task struct {
+ Runner
+
+ index int
+ name string
+ done chan error
+ dep map[*task]bool
+}
+
+func newTask(index int, name string, v cue.Value) (*task, error) {
+ kind, err := v.Lookup("kind").String()
+ if err != nil {
+ return nil, err
+ }
+ rf, ok := runners[kind]
+ if !ok {
+ return nil, fmt.Errorf("runner of kind %q not found", kind)
+ }
+ runner, err := rf(v)
+ if err != nil {
+ return nil, err
+ }
+ return &task{
+ Runner: runner,
+ index: index,
+ name: name,
+ done: make(chan error),
+ dep: make(map[*task]bool),
+ }, nil
+}
+
+// A Runner defines a command type.
+type Runner interface {
+ // Init is called with the original configuration before any task is run.
+ // As a result, the configuration may be incomplete, but allows some
+ // validation before tasks are kicked off.
+ // Init(v cue.Value)
+
+ // Runner runs given the current value and returns a new value which is to
+ // be unified with the original result.
+ Run(ctx context.Context, v cue.Value) (results interface{}, err error)
+}
+
+// A RunnerFunc creates a Runner.
+type RunnerFunc func(v cue.Value) (Runner, error)
+
+var runners = map[string]RunnerFunc{
+ "print": newPrintCmd,
+ "exec": newExecCmd,
+ "http": newHTTPCmd,
+ "testserver": newTestServerCmd,
+}
+
+type printCmd struct{}
+
+func newPrintCmd(v cue.Value) (Runner, error) {
+ return &printCmd{}, nil
+}
+
+// TODO: get rid of this hack
+var testOut io.Writer
+
+func (c *printCmd) Run(ctx context.Context, v cue.Value) (res interface{}, err error) {
+ str, err := v.Lookup("text").String()
+ if err != nil {
+ return nil, err
+ }
+ if testOut != nil {
+ fmt.Fprintln(testOut, str)
+ } else {
+ fmt.Println(str)
+ }
+ return nil, nil
+}
+
+type execCmd struct{}
+
+func newExecCmd(v cue.Value) (Runner, error) {
+ return &execCmd{}, nil
+}
+
+func (c *execCmd) Run(ctx context.Context, v cue.Value) (res interface{}, err error) {
+ // TODO: set environment variables, if defined.
+ var bin string
+ var args []string
+ switch v := v.Lookup("cmd"); v.Kind() {
+ case cue.StringKind:
+ str, _ := v.String()
+ if str == "" {
+ return cue.Value{}, errors.New("empty command")
+ }
+ list := strings.Fields(str)
+ bin = list[0]
+ for _, s := range list[1:] {
+ args = append(args, s)
+ }
+
+ 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
+ }
+ for list.Next() {
+ str, err := list.Value().String()
+ if err != nil {
+ return cue.Value{}, err
+ }
+ args = append(args, str)
+ }
+ }
+
+ cmd := exec.CommandContext(ctx, bin, args...)
+
+ if v := v.Lookup("stdin"); v.IsValid() {
+ if cmd.Stdin, err = v.Reader(); err != nil {
+ return nil, fmt.Errorf("cue: %v", err)
+ }
+ }
+ captureOut := !v.Lookup("stdout").IsNull()
+ if !captureOut {
+ cmd.Stdout = os.Stdout
+ }
+ captureErr := !v.Lookup("stderr").IsNull()
+ if captureErr {
+ cmd.Stderr = os.Stderr
+ }
+
+ update := map[string]interface{}{}
+ var stdout, stderr []byte
+ if captureOut {
+ stdout, err = cmd.Output()
+ update["stdout"] = string(stdout)
+ } else {
+ err = cmd.Run()
+ }
+ update["success"] = err == nil
+ if err != nil {
+ if exit, ok := err.(*exec.ExitError); ok && captureErr {
+ stderr = exit.Stderr
+ } else {
+ return nil, fmt.Errorf("cue: %v", err)
+ }
+ }
+ if captureErr {
+ update["stderr"] = string(stderr)
+ }
+ return update, nil
+}
+
+type httpCmd struct{}
+
+func newHTTPCmd(v cue.Value) (Runner, error) {
+ return &httpCmd{}, nil
+}
+
+func (c *httpCmd) Run(ctx context.Context, v cue.Value) (res interface{}, err error) {
+ // v.Validate()
+ var header, trailer http.Header
+ method := lookupString(v, "method")
+ u := lookupString(v, "url")
+ var r io.Reader
+ if obj := v.Lookup("request"); v.Exists() {
+ if v := obj.Lookup("body"); v.Exists() {
+ r, err = v.Reader()
+ if err != nil {
+ return nil, err
+ }
+ }
+ if header, err = parseHeaders(obj, "header"); err != nil {
+ return nil, err
+ }
+ if trailer, err = parseHeaders(obj, "trailer"); err != nil {
+ return nil, err
+ }
+ }
+ req, err := http.NewRequest(method, u, r)
+ if err != nil {
+ return nil, err
+ }
+ req.Header = header
+ req.Trailer = trailer
+
+ // TODO:
+ // - retry logic
+ // - TLS certs
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ // parse response body and headers
+ return map[string]interface{}{
+ "response": map[string]interface{}{
+ "body": string(b),
+ "header": resp.Header,
+ "trailer": resp.Trailer,
+ },
+ }, err
+}
+
+func parseHeaders(obj cue.Value, label string) (http.Header, error) {
+ m := obj.Lookup(label)
+ if !m.Exists() {
+ return nil, nil
+ }
+ iter, err := m.Fields()
+ if err != nil {
+ return nil, err
+ }
+ var h http.Header
+ for iter.Next() {
+ str, err := iter.Value().String()
+ if err != nil {
+ return nil, err
+ }
+ h.Add(iter.Label(), str)
+ }
+ return h, nil
+}
+
+func isValid(v cue.Value) bool {
+ return v.Kind() == cue.BottomKind
+}
+
+var testOnce sync.Once
+
+func newTestServerCmd(v cue.Value) (Runner, error) {
+ server := ""
+ testOnce.Do(func() {
+ s := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, req *http.Request) {
+ data, _ := ioutil.ReadAll(req.Body)
+ d := map[string]interface{}{
+ "data": string(data),
+ "when": "now",
+ }
+ enc := json.NewEncoder(w)
+ enc.Encode(d)
+ }))
+ server = s.URL
+ })
+ return testServerCmd(server), nil
+}
+
+type testServerCmd string
+
+func (s testServerCmd) Run(ctx context.Context, v cue.Value) (x interface{}, err error) {
+ return map[string]interface{}{"url": string(s)}, nil
+}
diff --git a/cmd/cue/cmd/custom_test.go b/cmd/cue/cmd/custom_test.go
new file mode 100644
index 0000000..f6e78aa
--- /dev/null
+++ b/cmd/cue/cmd/custom_test.go
@@ -0,0 +1,74 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "strconv"
+ "strings"
+ "testing"
+)
+
+func TestIsCyclic(t *testing.T) {
+ testCases := []struct {
+ // semi-colon-separated list of nodes with comma-separated list
+ // of dependencies.
+ tasks string
+ cycle bool
+ }{{
+ tasks: "",
+ }, {
+ tasks: "0",
+ cycle: true,
+ }, {
+ tasks: "1; 0",
+ cycle: true,
+ }, {
+ tasks: "1; 2; 3; 4;",
+ }, {
+ tasks: "1; 2; ; 4; 5; ",
+ }, {
+ tasks: "1; 2; 3; 4; 0",
+ cycle: true,
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.tasks, func(t *testing.T) {
+ deps := strings.Split(tc.tasks, ";")
+ tasks := make([]*task, len(deps))
+ for i := range tasks {
+ tasks[i] = &task{index: i, dep: map[*task]bool{}}
+ }
+ for i, d := range deps {
+ if d == "" {
+ continue
+ }
+ for _, num := range strings.Split(d, ",") {
+ num = strings.TrimSpace(num)
+ if num == "" {
+ continue
+ }
+ x, err := strconv.Atoi(num)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("%d -> %d", i, x)
+ tasks[i].dep[tasks[x]] = true
+ }
+ }
+ if got := isCyclic(tasks); got != tc.cycle {
+ t.Errorf("got %v; want %v", got, tc.cycle)
+ }
+ })
+ }
+}
diff --git a/cmd/cue/cmd/eval.go b/cmd/cue/cmd/eval.go
new file mode 100644
index 0000000..af90e21
--- /dev/null
+++ b/cmd/cue/cmd/eval.go
@@ -0,0 +1,191 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "text/tabwriter"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/format"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+ "github.com/spf13/cobra"
+)
+
+// evalCmd represents the eval command
+var evalCmd = &cobra.Command{
+ Use: "eval",
+ Short: "evaluate and print a configuration",
+ Long: `eval evaluates, validates, and prints a configuration.
+
+Printing is skipped if validation fails.
+
+The --expression flag is used to evaluate an expression within the
+configuration file, instead of the entire configuration file itself.
+
+Examples:
+
+ $ cat <<EOF > foo.cue
+ a: [ "a", "b", "c" ]
+ EOF
+
+ $ cue eval foo.cue -e a[0] -e a[2]
+ "a"
+ "c"
+`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ instances := buildFromArgs(cmd, args)
+
+ var exprs []ast.Expr
+ for _, e := range *expressions {
+ expr, err := parser.ParseExpr(token.NewFileSet(), "<expression flag>", e)
+ if err != nil {
+ return err
+ }
+ exprs = append(exprs, expr)
+ }
+
+ tw := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 1, ' ', 0)
+ defer tw.Flush()
+ for _, inst := range instances {
+ // TODO: retrieve the fileset from the instance. Probably using an
+ // internal structure.
+ p := evalPrinter{w: tw, fset: nil}
+ if exprs == nil {
+ p.print(inst.Value())
+ fmt.Fprintln(tw)
+ }
+ for _, e := range exprs {
+ p.print(inst.Eval(e))
+ fmt.Fprintln(tw)
+ }
+ }
+ return nil
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(evalCmd)
+
+ expressions = evalCmd.Flags().StringArrayP("expression", "e", nil, "evaluate this expression only")
+
+}
+
+var (
+ expressions *[]string
+)
+
+type evalPrinter struct {
+ w io.Writer
+ fset *token.FileSet
+ indent int
+ newline bool
+ formfeed bool
+}
+
+type ws byte
+
+const (
+ unindent = -1
+ indent = 1
+ newline ws = '\n'
+ vtab ws = '\v'
+ space ws = ' '
+
+ // maxDiffLen is the maximum different in length for object keys for which
+ // to still align keys and values.
+ maxDiffLen = 5
+)
+
+func (p *evalPrinter) print(args ...interface{}) {
+ for _, a := range args {
+ if d, ok := a.(int); ok {
+ p.indent += d
+ continue
+ }
+ if p.newline {
+ nl := '\n'
+ if p.formfeed {
+ nl = '\f'
+ }
+ p.w.Write([]byte{byte(nl)})
+ fmt.Fprint(p.w, strings.Repeat(" ", int(p.indent)))
+ p.newline = false
+ }
+ switch v := a.(type) {
+ case ws:
+ switch v {
+ case newline:
+ p.newline = true
+ default:
+ p.w.Write([]byte{byte(v)})
+ }
+ case string:
+ fmt.Fprint(p.w, v)
+ case cue.Value:
+ switch v.Kind() {
+ case cue.StructKind:
+ iter, err := v.AllFields()
+ must(err)
+ lastLen := 0
+ p.print("{", indent, newline)
+ for iter.Next() {
+ value := iter.Value()
+ key := iter.Label()
+ newLen := len([]rune(key)) // TODO: measure cluster length.
+ if lastLen > 0 && abs(lastLen, newLen) > maxDiffLen {
+ p.formfeed = true
+ } else {
+ k := value.Kind()
+ p.formfeed = k == cue.StructKind || k == cue.ListKind
+ }
+ p.print(key, ":", vtab, value, newline)
+ p.formfeed = false
+ lastLen = newLen
+ }
+ p.print(unindent, "}")
+ case cue.ListKind:
+ list, err := v.List()
+ must(err)
+ p.print("[", indent, newline)
+ for list.Next() {
+ p.print(list.Value(), newline)
+ }
+ p.print(unindent, "]")
+ default:
+ format.Node(p.w, v.Syntax())
+ }
+ }
+ }
+}
+
+func abs(a, b int) int {
+ a -= b
+ if a < 0 {
+ return -a
+ }
+ return a
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/cmd/cue/cmd/eval_test.go b/cmd/cue/cmd/eval_test.go
new file mode 100644
index 0000000..67e7c41
--- /dev/null
+++ b/cmd/cue/cmd/eval_test.go
@@ -0,0 +1,21 @@
+// Copyright 2018 The 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 cmd
+
+import "testing"
+
+func TestEval(t *testing.T) {
+ runCommand(t, evalCmd.RunE, "eval")
+}
diff --git a/cmd/cue/cmd/export.go b/cmd/cue/cmd/export.go
new file mode 100644
index 0000000..d4f8fa0
--- /dev/null
+++ b/cmd/cue/cmd/export.go
@@ -0,0 +1,95 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "encoding/json"
+
+ "github.com/spf13/cobra"
+)
+
+// exportCmd represents the emit command
+var exportCmd = &cobra.Command{
+ Use: "export",
+ Short: "output data in a standard format",
+ Long: `export evaluates the configuration found in the current
+directory and prints the emit value to stdout.
+
+Examples:
+Evaluated and emit
+
+ # a single file
+ cue export config.cue
+
+ # multiple files: these are combined at the top-level. Order doesn't matter.
+ cue export file1.cue foo/file2.cue
+
+ # all files within the "mypkg" package: this includes all files in the
+ # current directory and its ancestor directories that are marked with the
+ # same package.
+ cue export -p cloud
+
+ # the -p flag can be omitted if the directory only contains files for
+ # the "mypkg" package.
+ cue export
+
+Emit value:
+For CUE files, the generated configuration is derived from the top-level
+single expression, the emit value. For example, the file
+
+ // config.cue
+ arg1: 1
+ arg2: "my string"
+
+ {
+ a: arg1
+ b: arg2
+ }
+
+yields the following JSON:
+
+ {
+ "a": 1,
+ "b", "my string"
+ }
+
+In absence of arguments, the current directory is loaded as a package instance.
+A package instance for a directory contains all files in the directory and its
+ancestor directories, up to the module root, belonging to the same package.
+If the package is not explicitly defined by the '-p' flag, it must be uniquely
+defined by the files in the current directory.
+`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ instances := buildFromArgs(cmd, args)
+ e := json.NewEncoder(cmd.OutOrStdout())
+ e.SetIndent("", " ")
+ e.SetEscapeHTML(*escape)
+
+ root := instances[0].Value()
+ must(e.Encode(root))
+ },
+}
+
+var (
+ escape *bool
+)
+
+func init() {
+ RootCmd.AddCommand(exportCmd)
+
+ exportCmd.Flags().StringP("output", "o", "json", "output format (json only for now)")
+ escape = exportCmd.Flags().BoolP("escape", "e", false, "use HTML escaping")
+}
diff --git a/cmd/cue/cmd/fmt.go b/cmd/cue/cmd/fmt.go
new file mode 100644
index 0000000..7ee1fe7
--- /dev/null
+++ b/cmd/cue/cmd/fmt.go
@@ -0,0 +1,73 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "io/ioutil"
+ "os"
+
+ "cuelang.org/go/cue/format"
+ "cuelang.org/go/cue/load"
+ "github.com/spf13/cobra"
+)
+
+// fmtCmd represents the fmt command
+var fmtCmd = &cobra.Command{
+ Use: "fmt [-s] [packages]",
+ Short: "formats CUE configuration files.",
+ Long: `Fmt formats the given files or the files for the given packages in place
+`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ for _, inst := range load.Instances(args, nil) {
+ all := []string{}
+ all = append(all, inst.CUEFiles...)
+ all = append(all, inst.ToolCUEFiles...)
+ all = append(all, inst.TestCUEFiles...)
+ for _, path := range all {
+ fullpath := inst.Abs(path)
+
+ stat, err := os.Stat(fullpath)
+ if err != nil {
+ return err
+ }
+
+ b, err := ioutil.ReadFile(fullpath)
+ if err != nil {
+ return err
+ }
+
+ opts := []format.Option{}
+ if *fSimplify {
+ opts = append(opts, format.Simplify())
+ }
+
+ b, err = format.Source(b, opts...)
+ if err != nil {
+ return err
+ }
+
+ err = ioutil.WriteFile(fullpath, b, stat.Mode())
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(fmtCmd)
+}
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
new file mode 100644
index 0000000..5ead4fd
--- /dev/null
+++ b/cmd/cue/cmd/import.go
@@ -0,0 +1,761 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/encoding"
+ "cuelang.org/go/cue/format"
+ "cuelang.org/go/cue/load"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal"
+ "cuelang.org/go/internal/third_party/yaml"
+ "github.com/spf13/cobra"
+ "golang.org/x/sync/errgroup"
+)
+
+// importCmd represents the import command
+var importCmd = &cobra.Command{
+ Use: "import",
+ Short: "convert other data formats to CUE files",
+ Long: `import converts other data formats, like JSON and YAML to CUE files
+
+The following file formats are currently supported:
+
+ Format Extensions
+ JSON .json .jsonl .ndjson
+ YAML .yaml .yml
+
+Files can either be specified explicitly, or inferred from the specified
+packages. In either case, the file extension is replaced with .cue. It will
+fail if the file already exists by default. The -f flag overrides this.
+
+Examples:
+
+ # Convert individual files:
+ $ cue import foo.json bar.json # create foo.yaml and bar.yaml
+
+ # Convert all json files in the indicated directories:
+ $ cue import ./... -type=json
+
+
+The -path flag
+
+By default the parsed files are included as emit values. This default can be
+overridden by specifying a sequence of labels as you would in a CUE file.
+An identifier or string label are interpreted as usual. A label expression is
+evaluated within the context of the imported file. label expressions may also
+refer to builtin packages, which will be implicitly imported.
+
+
+Handling multiple documents or streams
+
+To handle Multi-document files, such as concatenated JSON objects or
+YAML files with document separators (---) the user must specify either the
+-path, -list, or -files flag. The -path flag assign each element to a path
+(identical paths are treated as usual); -list concatenates the entries, and
+-files causes each entry to be written to a different file. The -files flag
+may only be used if files are explicitly imported. The -list flag may be
+used in combination with the -path flag, concatenating each entry to the
+mapped location.
+
+
+Examples:
+
+ $ cat <<EOF > foo.yaml
+ kind: Service
+ name: booster
+ EOF
+
+ # include the parsed file as an emit value:
+ $ cue import foo.yaml
+ $ cat foo.cue
+ {
+ kind: Service
+ name: booster
+ }
+
+ # include the parsed file at the root of the CUE file:
+ $ cue import -f -l "" foo.yaml
+ $ cat foo.cue
+ kind: Service
+ name: booster
+
+ # include the import config at the mystuff path
+ $ cue import -f -l mystuff foo.yaml
+ $ cat foo.cue
+ myStuff: {
+ kind: Service
+ name: booster
+ }
+
+ # append another object to the input file
+ $ cat <<EOF >> foo.yaml
+ ---
+ kind: Deployment
+ name: booster
+ replicas: 1
+
+ # base the path values on th input
+ $ cue import -f -l '"\(strings.ToLower(kind))" "\(x.name)"' foo.yaml
+ $ cat foo.cue
+ service booster: {
+ kind: "Service"
+ name: "booster"
+ }
+
+ deployment booster: {
+ kind: "Deployment"
+ name: "booster
+ replicas: 1
+ }
+
+ # base the path values on th input
+ $ cue import -f -list -foo.yaml
+ $ cat foo.cue
+ [{
+ kind: "Service"
+ name: "booster"
+ }, {
+ kind: "Deployment"
+ name: "booster
+ replicas: 1
+ }]
+
+ # base the path values on th input
+ $ cue import -f -list -l '"\(strings.ToLower(kind))"' foo.yaml
+ $ cat foo.cue
+ service: [{
+ kind: "Service"
+ name: "booster"
+ }
+ deployment: [{
+ kind: "Deployment"
+ name: "booster
+ replicas: 1
+ }]
+
+
+Embedded data files
+
+The --recursive or -R flag enables the parsing of fields that are string
+representations of data formats themselves. A field that can be parsed is
+replaced with a call encoding the data from a structured form that is placed
+in a sibling field.
+
+It is also possible to recursively hoist data formats:
+
+Example:
+ $ cat <<EOF > example.json
+ "a": {
+ "data": '{ "foo": 1, "bar": 2 }',
+ }
+ EOF
+
+ $ cue import -R example.json
+ $ cat example.cue
+ import "encode/json"
+
+ a: {
+ data: json.Encode(_data),
+ _data: {
+ foo: 1
+ bar: 2
+ }
+ }
+`,
+ RunE: runImport,
+}
+
+func init() {
+ RootCmd.AddCommand(importCmd)
+
+ out = importCmd.Flags().StringP("out", "o", "", "alternative output or - for stdout")
+ name = importCmd.Flags().StringP("name", "n", "", "glob filter for file names")
+ typ = importCmd.Flags().String("type", "", "only apply to files of this type")
+ force = importCmd.Flags().BoolP("force", "f", false, "force overwriting existing files")
+ dryrun = importCmd.Flags().Bool("dryrun", false, "force overwriting existing files")
+
+ node = importCmd.Flags().StringP("path", "l", "", "path to include root")
+ list = importCmd.Flags().Bool("list", false, "concatenate multiple objects into a list")
+ files = importCmd.Flags().Bool("files", false, "split multiple entries into different files")
+ parseStrings = importCmd.Flags().BoolP("recursive", "R", false, "recursively parse string values")
+
+ importCmd.Flags().String("fix", "", "apply given fix")
+}
+
+var (
+ force *bool
+ name *string
+ typ *string
+ node *string
+ out *string
+ dryrun *bool
+ list *bool
+ files *bool
+ parseStrings *bool
+)
+
+type importFunc func(path string, r io.Reader) ([]ast.Expr, error)
+
+type encodingInfo struct {
+ fn importFunc
+ typ string
+}
+
+var (
+ jsonEnc = &encodingInfo{handleJSON, "json"}
+ yamlEnc = &encodingInfo{handleYAML, "yaml"}
+)
+
+func getExtInfo(ext string) *encodingInfo {
+ enc := encoding.MapExtension(ext)
+ if enc == nil {
+ return nil
+ }
+ switch enc.Name() {
+ case "json":
+ return jsonEnc
+ case "yaml":
+ return yamlEnc
+ }
+ return nil
+}
+
+func runImport(cmd *cobra.Command, args []string) error {
+ log.SetOutput(cmd.OutOrStderr())
+
+ var group errgroup.Group
+
+ group.Go(func() error {
+ if len(args) > 0 && len(filepath.Ext(args[0])) > len(".") {
+ for _, a := range args {
+ group.Go(func() error { return handleFile(cmd, *fPackage, a) })
+ }
+ return nil
+ }
+
+ done := map[string]bool{}
+
+ inst := load.Instances(args, &load.Config{DataFiles: true})
+ for _, pkg := range inst {
+ pkgName := *fPackage
+ if pkgName == "" {
+ pkgName = pkg.PkgName
+ }
+ if pkgName == "" && len(inst) > 1 {
+ return fmt.Errorf("must specify package name with the -p flag")
+ }
+ dir := pkg.Dir
+ if err := pkg.Err; err != nil {
+ return err
+ }
+ if done[dir] {
+ continue
+ }
+ done[dir] = true
+
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ return err
+ }
+ for _, file := range files {
+ ext := filepath.Ext(file.Name())
+ if enc := getExtInfo(ext); enc == nil || (*typ != "" && *typ != enc.typ) {
+ continue
+ }
+ path := filepath.Join(dir, file.Name())
+ group.Go(func() error { return handleFile(cmd, pkgName, path) })
+ }
+ }
+ return nil
+ })
+
+ err := group.Wait()
+ if err != nil {
+ return fmt.Errorf("Import failed: %v", err)
+ }
+ return nil
+}
+
+func handleFile(cmd *cobra.Command, pkg, filename string) error {
+ re, err := regexp.Compile(*name)
+ if err != nil {
+ return err
+ }
+ if !re.MatchString(filepath.Base(filename)) {
+ return nil
+ }
+ f, err := os.Open(filename)
+ if err != nil {
+ return fmt.Errorf("error opening file: %v", err)
+ }
+ defer f.Close()
+
+ ext := filepath.Ext(filename)
+ handler := getExtInfo(ext)
+
+ if handler == nil {
+ return fmt.Errorf("unsupported extension %q", ext)
+ }
+ objs, err := handler.fn(filename, f)
+ if err != nil {
+ return err
+ }
+
+ if *files {
+ for i, f := range objs {
+ err := combineExpressions(cmd, pkg, newName(filename, i), f)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ } else if len(objs) > 1 {
+ if !*list && *node == "" && !*files {
+ return fmt.Errorf("list, flag, or files flag needed to handle multiple objects in file %q", filename)
+ }
+ }
+ return combineExpressions(cmd, pkg, newName(filename, 0), objs...)
+}
+
+func combineExpressions(cmd *cobra.Command, pkg, cueFile string, objs ...ast.Expr) error {
+ if *out != "" {
+ cueFile = *out
+ }
+ if cueFile != "-" {
+ switch _, err := os.Stat(cueFile); {
+ case os.IsNotExist(err):
+ case err == nil:
+ if !*force {
+ log.Printf("skipping file %q: already exists", cueFile)
+ return nil
+ }
+ default:
+ return fmt.Errorf("error creating file: %v", err)
+ }
+ }
+
+ f := &ast.File{}
+ if pkg != "" {
+ f.Name = ast.NewIdent(pkg)
+ }
+
+ h := hoister{
+ fields: map[string]bool{},
+ altNames: map[string]*ast.Ident{},
+ }
+
+ index := newIndex()
+ for _, expr := range objs {
+ if *parseStrings {
+ h.hoist(expr)
+ }
+
+ // Compute a path different from root.
+ var pathElems []ast.Label
+
+ switch {
+ case *node != "":
+ inst, err := cue.FromExpr(nil, expr)
+ if err != nil {
+ return err
+ }
+
+ labels, err := parsePath(*node)
+ if err != nil {
+ return err
+ }
+ for _, l := range labels {
+ switch x := l.(type) {
+ case *ast.Interpolation:
+ v := inst.Eval(x)
+ if v.Kind() == cue.BottomKind {
+ return v.Err()
+ }
+ pathElems = append(pathElems, v.Syntax().(ast.Label))
+
+ case *ast.Ident, *ast.BasicLit:
+ pathElems = append(pathElems, x)
+
+ case *ast.ExprLabel:
+ v := inst.Eval(x.Label)
+ switch v.Kind() {
+ case cue.StringKind:
+ pathElems = append(pathElems, v.Syntax().(ast.Label))
+ case cue.NullKind,
+ cue.NumberKind,
+ cue.BoolKind:
+ pathElems = append(pathElems, newString(fmt.Sprint(v)))
+ case cue.BottomKind:
+ return v.Err()
+ default:
+ return fmt.Errorf("expression %q in path is not a label", internal.DebugStr(v.Syntax()))
+ }
+
+ case *ast.TemplateLabel:
+ return fmt.Errorf("template labels not supported in path flag")
+ }
+ }
+ }
+
+ if *list {
+ idx := index
+ for _, e := range pathElems {
+ idx = idx.label(e)
+ }
+ if idx.field.Value == nil {
+ idx.field.Value = &ast.ListLit{
+ Lbrack: token.Pos(token.NoSpace),
+ Rbrack: token.Pos(token.NoSpace),
+ }
+ }
+ list := idx.field.Value.(*ast.ListLit)
+ list.Elts = append(list.Elts, expr)
+ } else if len(pathElems) == 0 {
+ obj, ok := expr.(*ast.StructLit)
+ if !ok {
+ return fmt.Errorf("cannot map non-struct to object root")
+ }
+ f.Decls = append(f.Decls, obj.Elts...)
+ } else {
+ field := &ast.Field{Label: pathElems[0]}
+ f.Decls = append(f.Decls, field)
+ for _, e := range pathElems[1:] {
+ newField := &ast.Field{Label: e}
+ newVal := &ast.StructLit{Elts: []ast.Decl{newField}}
+ field.Value = newVal
+ field = newField
+ }
+ field.Value = expr
+ }
+ }
+
+ if len(h.altNames) > 0 {
+ imports := &ast.ImportDecl{}
+
+ for _, enc := range encoding.All() {
+ if ident, ok := h.altNames[enc.Name()]; ok {
+ short := enc.Name()
+ name := h.uniqueName(short, "")
+ ident.Name = name
+ if name == short {
+ ident = nil
+ }
+
+ path := fmt.Sprintf(`"encoding/%s"`, short)
+ imports.Specs = append(imports.Specs, &ast.ImportSpec{
+ Name: ident,
+ Path: &ast.BasicLit{Kind: token.STRING, Value: path},
+ })
+ }
+ }
+ f.Decls = append([]ast.Decl{imports}, f.Decls...)
+ }
+
+ if *list {
+ switch x := index.field.Value.(type) {
+ case *ast.StructLit:
+ f.Decls = append(f.Decls, x.Elts...)
+ case *ast.ListLit:
+ f.Decls = append(f.Decls, &ast.EmitDecl{Expr: x})
+ default:
+ panic("unreachable")
+ }
+ }
+
+ var buf bytes.Buffer
+ err := format.Node(&buf, f, format.Simplify())
+ if err != nil {
+ return fmt.Errorf("error formatting file: %v", err)
+ }
+
+ if cueFile == "-" {
+ _, err := io.Copy(cmd.OutOrStdout(), &buf)
+ return err
+ }
+ return ioutil.WriteFile(cueFile, buf.Bytes(), 0644)
+}
+
+type listIndex struct {
+ index map[string]*listIndex
+ file *ast.File // top-level only
+ field *ast.Field
+}
+
+func newIndex() *listIndex {
+ return &listIndex{
+ index: map[string]*listIndex{},
+ field: &ast.Field{},
+ }
+}
+
+func newString(s string) *ast.BasicLit {
+ return &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(s)}
+}
+
+func (x *listIndex) label(label ast.Label) *listIndex {
+ key := internal.DebugStr(label)
+ idx := x.index[key]
+ if idx == nil {
+ if x.field.Value == nil {
+ x.field.Value = &ast.StructLit{}
+ }
+ obj := x.field.Value.(*ast.StructLit)
+ newField := &ast.Field{Label: label}
+ obj.Elts = append(obj.Elts, newField)
+ idx = &listIndex{
+ index: map[string]*listIndex{},
+ field: newField,
+ }
+ x.index[key] = idx
+ }
+ return idx
+}
+
+func parsePath(exprs string) (p []ast.Label, err error) {
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "<path flag>", exprs+": _")
+ if err != nil {
+ return nil, fmt.Errorf("parser error in path %q: %v", exprs, err)
+ }
+
+ if len(f.Decls) != 1 {
+ return nil, errors.New("path flag must be a space-separated sequence of labels")
+ }
+
+ for d := f.Decls[0]; ; {
+ field, ok := d.(*ast.Field)
+ if !ok {
+ // This should never happen
+ return nil, errors.New("%q not a sequence of labels")
+ }
+
+ p = append(p, field.Label)
+
+ v, ok := field.Value.(*ast.StructLit)
+ if !ok {
+ break
+ }
+
+ if len(v.Elts) != 1 {
+ // This should never happen
+ return nil, errors.New("path value may not contain a struct")
+ }
+
+ d = v.Elts[0]
+ }
+ return p, nil
+}
+
+func newName(filename string, i int) string {
+ ext := filepath.Ext(filename)
+ filename = filename[:len(filename)-len(ext)]
+ if i > 0 {
+ filename += fmt.Sprintf("-%d", i)
+ }
+ filename += ".cue"
+ return filename
+}
+
+var fset = token.NewFileSet()
+
+func handleJSON(path string, r io.Reader) (objects []ast.Expr, err error) {
+ d := json.NewDecoder(r)
+
+ for {
+ var raw json.RawMessage
+ err := d.Decode(&raw)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("could not parse JSON: %v", err)
+ }
+ expr, err := parser.ParseExpr(fset, path, []byte(raw))
+ if err != nil {
+ return nil, fmt.Errorf("invalid input: %v %q", err, raw)
+ }
+ objects = append(objects, expr)
+ }
+ return objects, nil
+}
+
+func handleYAML(path string, r io.Reader) (objects []ast.Expr, err error) {
+ d, err := yaml.NewDecoder(fset, path, r)
+ if err != nil {
+ return nil, err
+ }
+ for i := 0; ; i++ {
+ expr, err := d.Decode()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ objects = append(objects, expr)
+ }
+ return objects, nil
+}
+
+type hoister struct {
+ fields map[string]bool
+ altNames map[string]*ast.Ident
+}
+
+func (h *hoister) hoist(expr ast.Expr) {
+ ast.Walk(expr, nil, func(n ast.Node) {
+ name := ""
+ switch x := n.(type) {
+ case *ast.Field:
+ name, _ = ast.LabelName(x.Label)
+ case *ast.Alias:
+ name = x.Ident.Name
+ }
+ if name != "" {
+ h.fields[name] = true
+ }
+ })
+
+ ast.Walk(expr, func(n ast.Node) bool {
+ switch n.(type) {
+ case *ast.ComprehensionDecl:
+ return false
+ }
+ return true
+
+ }, func(n ast.Node) {
+ obj, ok := n.(*ast.StructLit)
+ if !ok {
+ return
+ }
+ for i := 0; i < len(obj.Elts); i++ {
+ f, ok := obj.Elts[i].(*ast.Field)
+ if !ok {
+ continue
+ }
+
+ name, ident := ast.LabelName(f.Label)
+ if name == "" || !ident {
+ continue
+ }
+
+ lit, ok := f.Value.(*ast.BasicLit)
+ if !ok || lit.Kind != token.STRING {
+ continue
+ }
+
+ str, err := cue.Unquote(lit.Value)
+ if err != nil {
+ continue
+ }
+
+ expr, enc := tryParse(str)
+ if expr == nil {
+ continue
+ }
+
+ if h.altNames[enc.typ] == nil {
+ h.altNames[enc.typ] = &ast.Ident{Name: "cue"} // set name later
+ }
+
+ // found a replacable string
+ dataField := h.uniqueName(name, "cue")
+
+ f.Value = &ast.CallExpr{
+ Fun: &ast.SelectorExpr{
+ X: h.altNames[enc.typ],
+ Sel: ast.NewIdent("Marshal"),
+ },
+ Args: []ast.Expr{
+ ast.NewIdent(dataField),
+ },
+ }
+
+ obj.Elts = append(obj.Elts, nil)
+ copy(obj.Elts[i+1:], obj.Elts[i:])
+
+ obj.Elts[i+1] = &ast.Alias{
+ Ident: ast.NewIdent(dataField),
+ Expr: expr,
+ }
+
+ h.hoist(expr)
+ }
+ })
+}
+
+func tryParse(str string) (s ast.Expr, format *encodingInfo) {
+ b := []byte(str)
+ fset := token.NewFileSet()
+ if json.Valid(b) {
+ expr, err := parser.ParseExpr(fset, "", b)
+ if err != nil {
+ // TODO: report error
+ return nil, nil
+ }
+ switch expr.(type) {
+ case *ast.StructLit, *ast.ListLit:
+ default:
+ return nil, nil
+ }
+ return expr, jsonEnc
+ }
+
+ if expr, err := yaml.Unmarshal(fset, "", b); err == nil {
+ switch expr.(type) {
+ case *ast.StructLit, *ast.ListLit:
+ default:
+ return nil, nil
+ }
+ return expr, yamlEnc
+ }
+
+ return nil, nil
+}
+
+func (h *hoister) uniqueName(base, typ string) string {
+ base = strings.Map(func(r rune) rune {
+ if unicode.In(r, unicode.L, unicode.N) {
+ return r
+ }
+ return '_'
+ }, base)
+
+ name := base
+ for {
+ if !h.fields[name] {
+ return name
+ }
+ name = typ + "_" + base
+ typ += "x"
+ }
+}
diff --git a/cmd/cue/cmd/import_test.go b/cmd/cue/cmd/import_test.go
new file mode 100644
index 0000000..caf74b7
--- /dev/null
+++ b/cmd/cue/cmd/import_test.go
@@ -0,0 +1,40 @@
+// Copyright 2018 The 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 cmd
+
+import "testing"
+
+func TestImport(t *testing.T) {
+ importCmd.ParseFlags([]string{
+ "-o", "-", "-f", "--files",
+ })
+ runCommand(t, importCmd.RunE, "import_files")
+
+ *files = false
+ importCmd.ParseFlags([]string{
+ "-f", "-l", `"\(strings.ToLower(kind))" "\(name)"`,
+ })
+ runCommand(t, importCmd.RunE, "import_path")
+
+ importCmd.ParseFlags([]string{
+ "-f", "-l", `"\(strings.ToLower(kind))"`, "--list",
+ })
+ runCommand(t, importCmd.RunE, "import_list")
+
+ importCmd.ParseFlags([]string{
+ "-f", "-l", `"\(strings.ToLower(kind))" "\(name)"`, "--recursive",
+ })
+ runCommand(t, importCmd.RunE, "import_hoiststr")
+}
diff --git a/cmd/cue/cmd/root.go b/cmd/cue/cmd/root.go
new file mode 100644
index 0000000..2f9dbdc
--- /dev/null
+++ b/cmd/cue/cmd/root.go
@@ -0,0 +1,185 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "fmt"
+ logger "log"
+ "os"
+
+ homedir "github.com/mitchellh/go-homedir"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+// TODO: commands
+// fix: rewrite/refactor configuration files
+// -i interactive: open diff and ask to update
+// serve: like cmd, but for servers
+// extract: extract cue from other languages, like proto and go.
+// gen: generate files for other languages
+// generate like go generate (also convert cue to go doc)
+// test load and fully evaluate test files.
+//
+// TODO: documentation of concepts
+// tasks the key element for cmd, serve, and fix
+
+var log = logger.New(os.Stderr, "", logger.Lshortfile)
+
+var cfgFile string
+
+// RootCmd represents the base command when called without any subcommands
+var RootCmd = &cobra.Command{
+ Use: "cue",
+ Short: "cue emits configuration files to user-defined commands.",
+ Long: `cue evaluates CUE files, an extension of JSON, and sends them
+to user-defined commands for processing.
+
+Commands are defined in CUE as follows:
+
+ command deploy: {
+ cmd: "kubectl"
+ args: [ "-f", "deploy" ]
+ in: json.Encode($) // encode the emitted configuration.
+ }
+
+cue can also combine the results of http or grpc request with the input
+configuration for further processing. For more information on defining commands
+run 'gcfg help commands' or go to cuelang.org/pkg/cmd.
+
+For more information on writing CUE configuration files see cuelang.org.`,
+ // Uncomment the following line if your bare application
+ // has an action associated with it:
+ // Run: func(cmd *cobra.Command, args []string) { },
+
+ SilenceUsage: true,
+}
+
+// Execute adds all child commands to the root command sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+ log.SetFlags(0)
+ // Three categories of commands:
+ // - normal
+ // - user defined
+ // - help
+ // For the latter two, we need to use the default loading.
+ defer func() {
+ switch err := recover(); err {
+ case nil:
+ case panicSentinel:
+ log.Fatal(err)
+ os.Exit(1)
+ default:
+ panic(err)
+ }
+ // We use panic to escape, instead of os.Exit
+ }()
+ if args := os.Args[1:]; len(args) >= 1 && args[0] != "help" {
+ // TODO: for now we only allow one instance. Eventually, we can allow
+ // more if they all belong to the same package and we merge them
+ // before computing commands.
+ if cmd, _, err := RootCmd.Find(args); err != nil || cmd == nil {
+ tools := buildTools(RootCmd, args[1:])
+ addCustom(RootCmd, commandSection, args[0], tools)
+ }
+
+ type subSpec struct {
+ name string
+ cmd *cobra.Command
+ }
+ sub := map[string]subSpec{
+ "cmd": {commandSection, cmdCmd},
+ // "serve": {"server", nil},
+ // "fix": {"fix", nil},
+ }
+ if sub, ok := sub[args[0]]; ok && len(args) >= 2 {
+ args = args[1:]
+ if len(args) == 0 {
+ tools := buildTools(RootCmd, args)
+ // list available commands
+ commands := tools.Lookup(sub.name)
+ i, err := commands.Fields()
+ must(err)
+ for i.Next() {
+ _, _ = addCustom(sub.cmd, sub.name, i.Label(), tools)
+ }
+ return // TODO: will this trigger the help?
+ }
+ tools := buildTools(RootCmd, args[1:])
+ _, err := addCustom(sub.cmd, sub.name, args[0], tools)
+ if err != nil {
+ log.Printf("%s %q is not defined", sub.name, args[0])
+ exit()
+ }
+ }
+ }
+ if err := RootCmd.Execute(); err != nil {
+ // log.Fatal(err)
+ os.Exit(1)
+ }
+}
+
+var panicSentinel = "terminating because of errors"
+
+func must(err error) {
+ if err != nil {
+ log.Print(err)
+ exit()
+ }
+}
+
+func exit() { panic(panicSentinel) }
+
+func init() {
+ cobra.OnInitialize(initConfig)
+
+ RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cue)")
+ RootCmd.PersistentFlags().Bool("root", false, "load a CUE package from its root")
+}
+
+var (
+ fDebug = RootCmd.PersistentFlags().Bool("debug", false, "give detailed error info")
+ fTrace = RootCmd.PersistentFlags().Bool("trace", false, "trace computation")
+ fDryrun = RootCmd.PersistentFlags().BoolP("dryrun", "n", false, "only run simulation")
+ fPackage = RootCmd.PersistentFlags().StringP("package", "p", "", "CUE package to evaluate")
+ fSimplify = RootCmd.PersistentFlags().BoolP("simplify", "s", false, "simplify output")
+)
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+ if cfgFile != "" {
+ // Use config file from the flag.
+ viper.SetConfigFile(cfgFile)
+ } else {
+ // Find home directory.
+ home, err := homedir.Dir()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+
+ // Search config in home directory with name ".cue" (without extension).
+ viper.AddConfigPath(home)
+ viper.SetConfigName(".cue")
+ }
+
+ viper.AutomaticEnv() // read in environment variables that match
+
+ // If a config file is found, read it in.
+ if err := viper.ReadInConfig(); err == nil {
+ fmt.Println("Using config file:", viper.ConfigFileUsed())
+ }
+}
diff --git a/cmd/cue/cmd/testdata/hello/cmd_echo.out b/cmd/cue/cmd/testdata/hello/cmd_echo.out
new file mode 100644
index 0000000..ea2fd5c
--- /dev/null
+++ b/cmd/cue/cmd/testdata/hello/cmd_echo.out
@@ -0,0 +1,2 @@
+Hello World!
+
diff --git a/cmd/cue/cmd/testdata/hello/data.cue b/cmd/cue/cmd/testdata/hello/data.cue
new file mode 100644
index 0000000..e226892
--- /dev/null
+++ b/cmd/cue/cmd/testdata/hello/data.cue
@@ -0,0 +1,3 @@
+package hello
+
+who: "World"
\ No newline at end of file
diff --git a/cmd/cue/cmd/testdata/hello/eval.out b/cmd/cue/cmd/testdata/hello/eval.out
new file mode 100644
index 0000000..8af3d0f
--- /dev/null
+++ b/cmd/cue/cmd/testdata/hello/eval.out
@@ -0,0 +1,4 @@
+{
+ who: "World"
+ message: "Hello World!"
+}
diff --git a/cmd/cue/cmd/testdata/hello/hello.cue b/cmd/cue/cmd/testdata/hello/hello.cue
new file mode 100644
index 0000000..f8f8654
--- /dev/null
+++ b/cmd/cue/cmd/testdata/hello/hello.cue
@@ -0,0 +1,3 @@
+package hello
+
+message: "Hello \(who)!" // who declared in data.cue
diff --git a/cmd/cue/cmd/testdata/hello/hello_tool.cue b/cmd/cue/cmd/testdata/hello/hello_tool.cue
new file mode 100644
index 0000000..b3a6f2d
--- /dev/null
+++ b/cmd/cue/cmd/testdata/hello/hello_tool.cue
@@ -0,0 +1,14 @@
+package hello
+
+command echo: {
+ task echo: {
+ kind: "exec"
+ cmd: "echo \(message)"
+ stdout: string
+ }
+
+ task display: {
+ kind: "print"
+ text: task.echo.stdout
+ }
+}
\ No newline at end of file
diff --git a/cmd/cue/cmd/testdata/import/import_files.out b/cmd/cue/cmd/testdata/import/import_files.out
new file mode 100644
index 0000000..df6d24d
--- /dev/null
+++ b/cmd/cue/cmd/testdata/import/import_files.out
@@ -0,0 +1,8 @@
+kind: "Service"
+name: "booster"
+kind: "Deployment"
+name: "booster"
+replicas: 1
+kind: "Service"
+name: "supplement"
+json: "[1, 2]"
diff --git a/cmd/cue/cmd/testdata/import/import_hoiststr.out b/cmd/cue/cmd/testdata/import/import_hoiststr.out
new file mode 100644
index 0000000..d01566f
--- /dev/null
+++ b/cmd/cue/cmd/testdata/import/import_hoiststr.out
@@ -0,0 +1,19 @@
+import _json "encoding/json"
+
+service: {
+ booster: [{
+ kind: "Service"
+ name: "booster"
+ }]
+ supplement: [{
+ kind: "Service"
+ name: "supplement"
+ json: _json.Marshal(cue_json)
+ cue_json = [1, 2]
+ }]
+}
+deployment booster: [{
+ kind: "Deployment"
+ name: "booster"
+ replicas: 1
+}]
diff --git a/cmd/cue/cmd/testdata/import/import_list.out b/cmd/cue/cmd/testdata/import/import_list.out
new file mode 100644
index 0000000..e071c0d
--- /dev/null
+++ b/cmd/cue/cmd/testdata/import/import_list.out
@@ -0,0 +1,13 @@
+service: [{
+ kind: "Service"
+ name: "booster"
+}, {
+ kind: "Service"
+ name: "supplement"
+ json: "[1, 2]"
+}]
+deployment: [{
+ kind: "Deployment"
+ name: "booster"
+ replicas: 1
+}]
diff --git a/cmd/cue/cmd/testdata/import/import_path.out b/cmd/cue/cmd/testdata/import/import_path.out
new file mode 100644
index 0000000..e59d747
--- /dev/null
+++ b/cmd/cue/cmd/testdata/import/import_path.out
@@ -0,0 +1,15 @@
+
+service booster: {
+ kind: "Service"
+ name: "booster"
+}
+deployment booster: {
+ kind: "Deployment"
+ name: "booster"
+ replicas: 1
+}
+service supplement: {
+ kind: "Service"
+ name: "supplement"
+ json: "[1, 2]"
+}
diff --git a/cmd/cue/cmd/testdata/import/services.cue b/cmd/cue/cmd/testdata/import/services.cue
new file mode 100644
index 0000000..e071c0d
--- /dev/null
+++ b/cmd/cue/cmd/testdata/import/services.cue
@@ -0,0 +1,13 @@
+service: [{
+ kind: "Service"
+ name: "booster"
+}, {
+ kind: "Service"
+ name: "supplement"
+ json: "[1, 2]"
+}]
+deployment: [{
+ kind: "Deployment"
+ name: "booster"
+ replicas: 1
+}]
diff --git a/cmd/cue/cmd/testdata/import/services.jsonl b/cmd/cue/cmd/testdata/import/services.jsonl
new file mode 100644
index 0000000..5527811
--- /dev/null
+++ b/cmd/cue/cmd/testdata/import/services.jsonl
@@ -0,0 +1,14 @@
+{
+ "kind": "Service",
+ "name": "booster"
+}
+{
+ "kind": "Deployment",
+ "name": "booster",
+ "replicas": 1
+}
+{
+ "kind": "Service",
+ "name": "supplement",
+ "json": "[1, 2]"
+}
\ No newline at end of file
diff --git a/cmd/cue/cmd/testdata/loaderr/loaderr.out b/cmd/cue/cmd/testdata/loaderr/loaderr.out
new file mode 100644
index 0000000..2921045
--- /dev/null
+++ b/cmd/cue/cmd/testdata/loaderr/loaderr.out
@@ -0,0 +1,3 @@
+cannot find package "non-existing":
+
+terminating because of errors
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
new file mode 100644
index 0000000..e2dc4ef
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
@@ -0,0 +1,3 @@
+not of right kind (number vs string):
+ $CWD/testdata/tasks/task_tool.cue:23:9
+
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_http.out b/cmd/cue/cmd/testdata/tasks/cmd_http.out
new file mode 100644
index 0000000..058b2c2
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/cmd_http.out
@@ -0,0 +1,2 @@
+{"data":"I'll be back!","when":"now"}
+
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_run.out b/cmd/cue/cmd/testdata/tasks/cmd_run.out
new file mode 100644
index 0000000..47ee769
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/cmd_run.out
@@ -0,0 +1,2 @@
+Hello world!
+
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_run_list.out b/cmd/cue/cmd/testdata/tasks/cmd_run_list.out
new file mode 100644
index 0000000..47ee769
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/cmd_run_list.out
@@ -0,0 +1,2 @@
+Hello world!
+
diff --git a/cmd/cue/cmd/testdata/tasks/eval.out b/cmd/cue/cmd/testdata/tasks/eval.out
new file mode 100644
index 0000000..8c85ba9
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/eval.out
@@ -0,0 +1,3 @@
+{
+ message: "Hello world!"
+}
diff --git a/cmd/cue/cmd/testdata/tasks/task.cue b/cmd/cue/cmd/testdata/tasks/task.cue
new file mode 100644
index 0000000..cb91d4f
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/task.cue
@@ -0,0 +1,3 @@
+package home
+
+message: "Hello world!"
diff --git a/cmd/cue/cmd/testdata/tasks/task_tool.cue b/cmd/cue/cmd/testdata/tasks/task_tool.cue
new file mode 100644
index 0000000..ce37bf1
--- /dev/null
+++ b/cmd/cue/cmd/testdata/tasks/task_tool.cue
@@ -0,0 +1,56 @@
+package home
+
+command run: runBase & {
+ task echo cmd: "echo \(message)"
+}
+
+command run_list: runBase & {
+ task echo cmd: ["echo", message]
+}
+
+// TODO: capture stdout and stderr for tests.
+command runRedirect: {
+ task echo: {
+ kind: "exec"
+ cmd: "echo \(message)"
+ stdout: null // should be automatic
+ }
+}
+
+command baddisplay: {
+ task display: {
+ kind: "print"
+ text: 42
+ }
+}
+
+command http: {
+ task testserver: {
+ kind: "testserver"
+ url: string
+ }
+ task http: {
+ kind: "http"
+ method: "POST"
+ url: task.testserver.url
+
+ request body: "I'll be back!"
+ response body: string // TODO: allow this to be a struct, parsing the body.
+ }
+ task print: {
+ kind: "print"
+ text: task.http.response.body
+ }
+}
+
+runBase: {
+ task echo: {
+ kind: "exec"
+ stdout: string
+ }
+
+ task display: {
+ kind: "print"
+ text: task.echo.stdout
+ }
+}
diff --git a/cmd/cue/cmd/testdata/trim/trim.cue b/cmd/cue/cmd/testdata/trim/trim.cue
new file mode 100644
index 0000000..816af3f
--- /dev/null
+++ b/cmd/cue/cmd/testdata/trim/trim.cue
@@ -0,0 +1,73 @@
+package trim
+
+foo <Name>: {
+ _value: string
+
+ a: 4
+ b: string
+ d: 8
+ e: "foo"
+ f: ">> \( _value ) <<"
+ n: 5
+
+ list: ["foo", 8.0]
+
+ struct: {a: 3.0}
+
+ sList: [{a: 8, b: string}, {a: 9, b: "foo" | string}]
+ rList: [{a: "a"}]
+ rcList: [{a: "a", c: b}]
+
+ t <Name>: {
+ x: 0..5
+ }
+}
+
+foo bar: {
+ _value: "here"
+
+ a: 4
+ b: "foo"
+ c: 45
+ e: string
+ f: ">> here <<"
+
+ // The template does not require that this field be an integer (it may be
+ // a float), and thus this field specified additional information and
+ // cannot be removed.
+ n: int
+
+ struct: {a: 3.0}
+
+ list: ["foo", float]
+
+ sList: [{a: 8, b: "foo"}, {b: "foo"}]
+ rList: [{a: string}]
+ rcList: [{a: "a", c: "foo"}]
+}
+
+foo baz: {}
+
+foo multipath: {
+ t <Name>: {
+ // Combined with the other template, we know the value must be 5 and
+ // thus the entry below can be eliminated.
+ x: 5..8
+ }
+
+ t u: {
+ x: 5
+ }
+}
+
+// TODO: top-level fields are currently not removed.
+group: {
+ comp [k]: v for k, v in foo
+
+ comp bar: {
+ a: 4
+ aa: 8 // new value
+ }
+
+ comp baz: {} // removed: fully implied by comprehension above
+}
\ No newline at end of file
diff --git a/cmd/cue/cmd/testdata/trim/trim.out b/cmd/cue/cmd/testdata/trim/trim.out
new file mode 100644
index 0000000..73f746e
--- /dev/null
+++ b/cmd/cue/cmd/testdata/trim/trim.out
@@ -0,0 +1,59 @@
+package trim
+
+foo <Name>: {
+ _value: string
+
+ a: 4
+ b: string
+ d: 8
+ e: "foo"
+ f: ">> \( _value) <<"
+ n: 5
+
+ list: ["foo", 8.0]
+
+ struct: {a: 3.0}
+
+ sList: [{a: 8, b: string}, {a: 9, b: "foo" | string}]
+ rList: [{a: "a"}]
+ rcList: [{a: "a", c: b}]
+
+ t <Name>: {
+ x: 0..5
+ }
+}
+
+foo bar: {
+ _value: "here"
+ b: "foo"
+ c: 45
+
+ // The template does not require that this field be an integer (it may be
+ // a float), and thus this field specified additional information and
+ // cannot be removed.
+ n: int
+
+ sList: [{b: "foo"}, {}]
+}
+
+foo baz: {}
+
+foo multipath: {
+ t <Name>: {
+ // Combined with the other template, we know the value must be 5 and
+ // thus the entry below can be eliminated.
+ x: 5..8
+ }
+
+ t u: {
+ }
+}
+
+// TODO: top-level fields are currently not removed.
+group: {
+ comp [k]: v for k, v in foo
+
+ comp bar: {
+ aa: 8 // new value
+ }
+}
diff --git a/cmd/cue/cmd/trim.go b/cmd/cue/cmd/trim.go
new file mode 100644
index 0000000..cac1c8d
--- /dev/null
+++ b/cmd/cue/cmd/trim.go
@@ -0,0 +1,551 @@
+// Copyright 2018 The 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 cmd
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strconv"
+ "strings"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/build"
+ "cuelang.org/go/cue/format"
+ "cuelang.org/go/cue/load"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal"
+ "github.com/spf13/cobra"
+)
+
+// TODO:
+// - remove the limitations mentioned in the documentation
+// - implement verification post-processing as extra safety
+
+// trimCmd represents the trim command
+var trimCmd = &cobra.Command{
+ Use: "trim",
+ Short: "remove superfluous fields",
+ Long: `trim removes fields from structs that are already defined by a template
+
+A field, struct, or list is removed if it is implied by a template, a list type
+value, a comprehension or any other implied content. It will modify the files
+in place.
+
+Limitations
+Removal is on a best effort basis. Some caveats:
+- Fields in implied content may refer to fields within the struct in which
+ they are included, but are only resolved on a best-effort basis.
+- Disjunctions that contain structs in implied content cannot be used to
+ remove fields.
+- There is currently no verification step: manual verification is required.
+
+Examples:
+
+ $ cat <<EOF > foo.cue
+ light <Name>: {
+ room: string
+ brightnessOff: 0.0 | 0..100.0
+ brightnessOn: 100.0 | 0..100.0
+ }
+
+ light ceiling50: {
+ room: "MasterBedroom"
+ brightnessOn: 100 // this line
+ brightnessOff: 0 // and this line will be removed
+ }
+ EOF
+
+ $ cue trim foo.cue
+ $ cat foo.cue
+ light <Name>: {
+ room: string
+ brightnessOff: 0.0 | 0..100.0
+ brightnessOn: 100.0 | 0..100.0
+ }
+
+ light ceiling50: {
+ room: "MasterBedroom"
+ }
+
+It is guaranteed that the resulting files give the same output as before the
+removal.
+`,
+ RunE: runTrim,
+}
+
+func init() {
+ RootCmd.AddCommand(trimCmd)
+ fOut = trimCmd.Flags().StringP("out", "o", "", "alternative output or - for stdout")
+}
+
+var (
+ fOut *string
+)
+
+func runTrim(cmd *cobra.Command, args []string) error {
+ log.SetOutput(cmd.OutOrStderr())
+
+ ctxt := build.NewContext(build.ParseOptions(parser.ParseComments))
+ fset := ctxt.FileSet()
+ binst := load.Instances(args, &load.Config{Context: ctxt})
+
+ instances := cue.Build(binst)
+ for _, inst := range instances {
+ if err := inst.Err; err != nil {
+ return err
+ }
+ }
+
+ if *fOut != "" && *fOut != "-" {
+ switch _, err := os.Stat(*fOut); {
+ case os.IsNotExist(err):
+ case err == nil:
+ default:
+ return fmt.Errorf("error creating file: %v", err)
+ }
+ }
+
+ for i, inst := range binst {
+
+ gen := newTrimSet(fset)
+ for _, f := range inst.Files {
+ gen.markNodes(f)
+ }
+
+ root := instances[i].Lookup()
+ rm := gen.trim("root", root, cue.Value{}, root)
+
+ if *fDryrun {
+ continue
+ }
+
+ for _, f := range inst.Files {
+ filename := f.Filename
+
+ f.Decls = gen.trimDecls(f.Decls, rm, root, false)
+
+ opts := []format.Option{}
+ if *fSimplify {
+ opts = append(opts, format.Simplify())
+ }
+
+ var buf bytes.Buffer
+ err := format.Node(&buf, f, opts...)
+ if err != nil {
+ return fmt.Errorf("error formatting file: %v", err)
+ }
+
+ if *fOut == "-" {
+ _, err := io.Copy(cmd.OutOrStdout(), &buf)
+ if err != nil {
+ return err
+ }
+ continue
+ } else if *fOut != "" {
+ filename = *fOut
+ }
+
+ err = ioutil.WriteFile(filename, buf.Bytes(), 0644)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+type trimSet struct {
+ fset *token.FileSet
+ stack []string
+ exclude map[ast.Node]bool // don't remove fields marked here
+ alwaysGen map[ast.Node]bool // node is always from a generated source
+}
+
+func newTrimSet(fset *token.FileSet) *trimSet {
+ return &trimSet{
+ fset: fset,
+ exclude: map[ast.Node]bool{},
+ alwaysGen: map[ast.Node]bool{},
+ }
+}
+
+func (t *trimSet) path() string {
+ return strings.Join(t.stack[1:], " ")
+}
+
+func (t *trimSet) traceMsg(msg string) {
+ if *fTrace {
+ fmt.Print(t.path())
+ msg = strings.TrimRight(msg, "\n")
+ msg = strings.Replace(msg, "\n", "\n ", -1)
+ fmt.Printf(": %s\n", msg)
+ }
+}
+
+func (t *trimSet) markNodes(n ast.Node) {
+ ast.Walk(n, nil, func(n ast.Node) {
+ switch x := n.(type) {
+ case *ast.Ident:
+ if x.Node != nil {
+ t.exclude[x.Node] = true
+ }
+ if x.Scope != nil {
+ t.exclude[x.Scope] = true
+ }
+
+ case *ast.ListLit:
+ if x.Type != nil {
+ t.markAlwaysGen(x.Type)
+ }
+
+ case *ast.Field:
+ if _, ok := x.Label.(*ast.TemplateLabel); ok {
+ t.markAlwaysGen(x.Value)
+ }
+
+ case *ast.ListComprehension, *ast.ComprehensionDecl:
+ t.markAlwaysGen(x)
+ }
+ })
+}
+
+func (t *trimSet) markAlwaysGen(n ast.Node) {
+ ast.Walk(n, func(n ast.Node) bool {
+ t.alwaysGen[n] = true
+ return true
+ }, nil)
+}
+
+func (t *trimSet) canRemove(n ast.Node) bool {
+ return !t.exclude[n] && !t.alwaysGen[n]
+}
+
+func isDisjunctionOfStruct(n ast.Node) bool {
+ switch x := n.(type) {
+ case *ast.BinaryExpr:
+ if x.Op == token.DISJUNCTION {
+ return hasStruct(x.X) || hasStruct(x.Y)
+ }
+ }
+ return false
+}
+
+func hasStruct(n ast.Node) bool {
+ hasStruct := false
+ ast.Walk(n, func(n ast.Node) bool {
+ if _, ok := n.(*ast.StructLit); ok {
+ hasStruct = true
+ }
+ return !hasStruct
+ }, nil)
+ return hasStruct
+}
+
+// trim strips fields from structs that would otherwise be generated by implied
+// content, such as templates, comprehensions, and list types.
+//
+// The algorithm walks the tree with two values in parallel: one for the full
+// configuration, and one for implied content. For each node in the tree it
+// determines the value of the implied content and that of the full value
+// and strips any of the non-implied fields if it subsumes the implied ones.
+//
+// There are a few gotchas:
+// - Fields in the implied content may refer to fields in the complete config.
+// To support this, incomplete fields are detected and evaluated within the
+// configuration.
+// - Templates are instantiated as a result of the declaration of concrete
+// sibling fields. Such fields should not be removed even if the instantiated
+// template completely subsumes such fields as the reason to instantiate
+// the template will disappear with it.
+// - As the parallel structure is different, it may resolve to different
+// default values. There is no support yet for selecting defaults of a value
+// based on another value without doing a full unification. So for now we
+// skip any disjunction containing structs.
+//
+func (t *trimSet) trim(label string, v, m, scope cue.Value) (rmSet []ast.Node) {
+ saved := t.stack
+ t.stack = append(t.stack, label)
+ defer func() { t.stack = saved }()
+
+ vSplit := v.Split()
+
+ // At the moment disjunctions of structs are note supported. Detect them and
+ // punt.
+ // TODO: support disjunctions.
+ mSplit := m.Split()
+ for _, v := range mSplit {
+ if isDisjunctionOfStruct(v.Source()) {
+ return
+ }
+ }
+
+ switch v.Kind() {
+ case cue.StructKind:
+ // TODO: merge template preprocessing with that of fields.
+
+ // Only keep the good parts of the template.
+ // Incoming structs may be incomplete resulting in errors. It is safe
+ // to ignore these. If there is an actual error, it will manifest in
+ // the evaluation of v.
+ in := cue.Value{}
+ gen := []ast.Node{}
+ for _, v := range mSplit {
+ // TODO: consider resolving incomplete values within the current
+ // scope, as we do for fields.
+ if v.IsValid() {
+ in = in.Unify(v)
+ }
+ // Collect generated nodes.
+ gen = append(gen, v.Source())
+ }
+
+ // Identify generated components and unify them with the mixin value.
+ for _, v := range v.Split() {
+ if src := v.Source(); t.alwaysGen[src] {
+ if w := in.Unify(v); w.Err() == nil {
+ in = w
+ }
+ gen = append(gen, src)
+ }
+ }
+
+ // Build map of mixin fields.
+ valueMap := map[key]cue.Value{}
+ for mIter, _ := in.AllFields(); mIter.Next(); {
+ valueMap[iterKey(mIter)] = mIter.Value()
+ }
+
+ fn := v.Template()
+
+ // Process fields.
+ rm := []ast.Node{}
+ for iter, _ := v.AllFields(); iter.Next(); {
+ mSub := valueMap[iterKey(iter)]
+ if fn != nil {
+ mSub = mSub.Unify(fn(iter.Label()))
+ }
+
+ removed := t.trim(iter.Label(), iter.Value(), mSub, v)
+ rm = append(rm, removed...)
+ }
+
+ if *fTrace {
+ w := &bytes.Buffer{}
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "value: ", v)
+ if in.Exists() {
+ fmt.Fprintln(w, "mixed in: ", in)
+ }
+ for _, v := range vSplit {
+ status := "[]"
+ src := v.Source()
+ if inNodes(rmSet, src) {
+ status = "[re]"
+ } else if t.alwaysGen[src] {
+ status = "[i]"
+ }
+ fmt.Fprintf(w, " %4s %v: %v %T\n", status, v.Pos(), internal.DebugStr(src), src)
+ }
+
+ t.traceMsg(w.String())
+ }
+
+ // Remove fields from source.
+ for _, v := range vSplit {
+ if src := v.Source(); !t.alwaysGen[src] {
+ switch x := src.(type) {
+ case *ast.File:
+ // TODO: use in instead?
+ x.Decls = t.trimDecls(x.Decls, rm, m, fn != nil)
+
+ case *ast.StructLit:
+ x.Elts = t.trimDecls(x.Elts, rm, m, fn != nil)
+ if len(x.Elts) == 0 && m.Exists() && t.canRemove(src) && !inNodes(gen, src) {
+ rmSet = append(rmSet, src)
+ }
+
+ default:
+ if len(t.stack) == 1 {
+ // TODO: fix this hack to pass down the fields to remove
+ return rm
+ }
+ }
+ }
+ }
+
+ case cue.ListKind:
+ mIter, _ := m.List()
+ i := 0
+ rmElem := []ast.Node{}
+ for iter, _ := v.List(); iter.Next(); i++ {
+ mIter.Next()
+ rm := t.trim(strconv.Itoa(i), iter.Value(), mIter.Value(), scope)
+ rmElem = append(rmElem, rm...)
+ }
+
+ // Signal the removal of lists of which all elements have been marked
+ // for removal.
+ for _, v := range vSplit {
+ if src := v.Source(); !t.alwaysGen[src] {
+ l, ok := src.(*ast.ListLit)
+ if !ok {
+ break
+ }
+ rmList := true
+ iter, _ := v.List()
+ for i := 0; i < len(l.Elts) && iter.Next(); i++ {
+ if !inNodes(rmElem, l.Elts[i]) {
+ rmList = false
+ break
+ }
+ }
+ if rmList && m.Exists() && t.canRemove(src) {
+ rmSet = append(rmSet, src)
+ }
+ }
+ }
+ fallthrough
+
+ default:
+ // Collect generated nodes.
+ in := cue.Value{}
+ gen := []ast.Node{}
+ for _, v := range mSplit {
+ if v.IsValid() {
+ in = in.Unify(v)
+ }
+ // Collect generated nodes.
+ gen = append(gen, v.Source())
+ }
+
+ for _, v := range vSplit {
+ src := v.Source()
+ if t.alwaysGen[src] || inNodes(gen, src) {
+ if v.IsIncomplete() {
+ // The template has an expression that cannot be fully
+ // resolved. Attempt to complete the expression by
+ // evaluting it within the struct to which the template
+ // is applied.
+ expr := v.Syntax()
+ // TODO: this only resolves references contained in scope.
+ v = internal.EvalExpr(scope, expr).(cue.Value)
+ }
+ in = in.Unify(v)
+ gen = append(gen, src)
+ }
+ }
+
+ // Mark any subsumed part that is covered by generated config.
+ if in.Err() == nil && v.Subsumes(in) {
+ for _, v := range vSplit {
+ src := v.Source()
+ if t.canRemove(src) && !inNodes(gen, src) {
+ rmSet = append(rmSet, src)
+ }
+ }
+ }
+
+ if *fTrace {
+ w := &bytes.Buffer{}
+ if len(rmSet) > 0 {
+ fmt.Fprint(w, "field: SUBSUMED\n")
+ } else {
+ fmt.Fprint(w, "field: \n")
+ }
+ fmt.Fprintln(w, "value: ", v)
+ if in.Exists() {
+ fmt.Fprintln(w, "mixed in: ", in)
+ }
+ for _, v := range vSplit {
+ status := "["
+ if inNodes(gen, v.Source()) {
+ status += "i"
+ }
+ if inNodes(rmSet, v.Source()) {
+ status += "r"
+ }
+ status += "]"
+ src := v.Source()
+ fmt.Fprintf(w, " %4s %v: %v\n", status, v.Pos(), internal.DebugStr(src))
+ }
+
+ t.traceMsg(w.String())
+ }
+ }
+ return rmSet
+}
+
+func (t *trimSet) trimDecls(decls []ast.Decl, rm []ast.Node, m cue.Value, fromTemplate bool) []ast.Decl {
+ a := make([]ast.Decl, 0, len(decls))
+
+ for _, d := range decls {
+ if f, ok := d.(*ast.Field); ok {
+ label, _ := ast.LabelName(f.Label)
+ v := m.Lookup(label)
+ if inNodes(rm, f.Value) && (!fromTemplate || v.Exists()) {
+ continue
+ }
+ }
+ a = append(a, d)
+ }
+ return a
+}
+
+func inNodes(a []ast.Node, n ast.Node) bool {
+ for _, e := range a {
+ if e == n {
+ return true
+ }
+ }
+ return false
+}
+
+func deleteNode(a []ast.Node, n ast.Node) []ast.Node {
+ k := 0
+ for _, e := range a {
+ if e != n {
+ a[k] = e
+ k++
+ }
+ }
+ return a[:k]
+}
+
+type key struct {
+ label string
+ hidden bool
+}
+
+func iterKey(v cue.Iterator) key {
+ return key{v.Label(), v.IsHidden()}
+}
+
+func nodeKey(v *ast.Field) key {
+ if v == nil {
+ return key{}
+ }
+ name, _ := ast.LabelName(v.Label)
+ hidden := false
+ if ident, ok := v.Label.(*ast.Ident); ok &&
+ strings.HasPrefix(ident.Name, "_") {
+ hidden = true
+ }
+ return key{name, hidden}
+}
diff --git a/cmd/cue/cmd/trim_test.go b/cmd/cue/cmd/trim_test.go
new file mode 100644
index 0000000..7b23082
--- /dev/null
+++ b/cmd/cue/cmd/trim_test.go
@@ -0,0 +1,22 @@
+// Copyright 2018 The 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 cmd
+
+import "testing"
+
+func TestTrim(t *testing.T) {
+ trimCmd.ParseFlags([]string{"-o", "-"})
+ runCommand(t, trimCmd.RunE, "trim")
+}
diff --git a/cmd/cue/main.go b/cmd/cue/main.go
new file mode 100644
index 0000000..2edd4b1
--- /dev/null
+++ b/cmd/cue/main.go
@@ -0,0 +1,21 @@
+// Copyright 2018 The 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 main
+
+import "cuelang.org/go/cmd/cue/cmd"
+
+func main() {
+ cmd.Execute()
+}
diff --git a/go.mod b/go.mod
index e180d70..5e108d3 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,12 @@
require (
github.com/cockroachdb/apd v1.1.0
github.com/ghodss/yaml v1.0.0
+ github.com/google/go-cmp v0.2.0
+ github.com/mitchellh/go-homedir v1.0.0
github.com/pkg/errors v0.8.0 // indirect
+ github.com/spf13/cobra v0.0.3
+ github.com/spf13/viper v1.3.1
golang.org/x/exp/errors v0.0.0-20181210123644-7d6377eee41f
+ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77
- gopkg.in/yaml.v2 v2.2.2 // indirect
)
diff --git a/go.sum b/go.sum
index 5293807..7c0bae2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,53 @@
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
+github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/exp/errors v0.0.0-20181210123644-7d6377eee41f h1:B/8yFg7PHSFdahc+fMB+RUy3if9GlZmexAbcdfCwREI=
golang.org/x/exp/errors v0.0.0-20181210123644-7d6377eee41f/go.mod h1:YgqsNsAu4fTvlab/7uiYK9LJrCIzKg/NiZUIH1/ayqo=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77 h1:s+6psEFi3o1QryeA/qyvUoVaHMCQkYVvZ0i2ZolwSJc=
golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/doc.go b/pkg/doc.go
index d4476d0..307e5cb 100644
--- a/pkg/doc.go
+++ b/pkg/doc.go
@@ -12,17 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-// Package pkg define CUE builtin packages.
+// Package pkg define CUE standard packages.
//
-// The CUE core packages are different, but closely mimic the Go core packages.
-// The types, values, and functions are defined as their Go equivalence and
-// mapped to CUE types.
+// Many of the standard packages are modeled after and generated from the Go
+// core packages. The types, values, and functions are defined as their Go
+// equivalence and mapped to CUE types.
//
// Beware that some packages are defined in lesser-precision types than are
// typically used in CUE and thus may lead to loss of precision.
//
-// All packages in these subdirectories are hermetic. That is, the results
-// do not change based on environment conditions. That is:
+// All packages except those defined in the tool subdirectory are hermetic,
+// that is depending only on a known set of inputs, and therefore can guarantee
+// reproducible results. That is:
//
// - no reading of files contents
// - no querying of the file system of any kind
@@ -31,6 +32,7 @@
// - only reproduceable random generators
//
// Hermetic configurations allow for fast and advanced analysis that otherwise
-// would not be possible or practical. The cue "cmd" command can be used to
-// mix in non-hermetic influences into configurations.
+// would not be possible or practical. The cue "cmd" command can be used to mix
+// in non-hermetic influences into configurations by using packages defined
+// in the tool subdirectory.
package pkg