cmd/cue/cmd: simplify usage of --path flag

Closes #176
Issue #193

Change-Id: I95609cec93b0429470f1d7342ab12d45fd9207c7
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4905
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
index ec6f45a..e79ae38 100644
--- a/cmd/cue/cmd/import.go
+++ b/cmd/cue/cmd/import.go
@@ -29,11 +29,9 @@
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
 
-	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/ast/astutil"
 	"cuelang.org/go/cue/encoding"
-	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/format"
 	"cuelang.org/go/cue/literal"
 	"cuelang.org/go/cue/load"
@@ -125,13 +123,13 @@
   }
 
   # include the parsed file at the root of the CUE file:
-  $ cue import -f -l "" foo.yaml
+  $ cue import -f foo.yaml
   $ cat foo.cue
   kind: Service
   name: booster
 
   # include the import config at the mystuff path
-  $ cue import -f -l mystuff foo.yaml
+  $ cue import -f -l '"mystuff"' foo.yaml
   $ cat foo.cue
   myStuff: {
       kind: Service
@@ -145,8 +143,8 @@
   name: booster
   replicas: 1
 
-  # base the path values on th input
-  $ cue import -f -l '"\(strings.ToLower(kind))" "\(x.name)"' foo.yaml
+  # base the path values on the input
+  $ cue import -f -l 'strings.ToLower(kind)' -l name foo.yaml
   $ cat foo.cue
   service: booster: {
       kind: "Service"
@@ -154,7 +152,7 @@
   }
 
   # base the path values on the input and file name
-  $ cue import -f --with-context -l '"\(path.Base(filename))" "\(data.kind)"' foo.yaml
+  $ cue import -f --with-context -l 'path.Base(filename)' -l data.kind foo.yaml
   $ cat foo.cue
   "foo.yaml": Service: {
       kind: "Service"
@@ -167,7 +165,7 @@
       replicas: 1
   }
 
-  # base the path values on th input
+  # include all files as list elements
   $ cue import -f -list -foo.yaml
   $ cat foo.cue
   [{
@@ -179,8 +177,8 @@
       replicas: 1
   }]
 
-  # base the path values on th input
-  $ cue import -f -list -l '"\(strings.ToLower(kind))"' foo.yaml
+  # collate files with the same path into a list
+  $ cue import -f -list -l 'strings.ToLower(kind)' foo.yaml
   $ cat foo.cue
   service: [{
       kind: "Service"
@@ -230,7 +228,7 @@
 	cmd.Flags().BoolP(string(flagForce), "f", false, "force overwriting existing files")
 	cmd.Flags().Bool(string(flagDryrun), false, "only run simulation")
 
-	cmd.Flags().StringP(string(flagPath), "l", "", "path to include root")
+	cmd.Flags().StringArrayP(string(flagPath), "l", nil, "CUE expression for single path component")
 	cmd.Flags().Bool(string(flagList), false, "concatenate multiple objects into a list")
 	cmd.Flags().Bool(string(flagFiles), false, "split multiple entries into different files")
 	cmd.Flags().BoolP(string(flagRecursive), "R", false, "recursively parse string values")
@@ -397,7 +395,7 @@
 		}
 		return nil
 	} else if len(objs) > 1 {
-		if !flagList.Bool(cmd) && flagPath.String(cmd) == "" && !flagFiles.Bool(cmd) {
+		if !flagList.Bool(cmd) && len(flagPath.StringArray(cmd)) == 0 && !flagFiles.Bool(cmd) {
 			return fmt.Errorf("list, flag, or files flag needed to handle multiple objects in file %q", filename)
 		}
 	}
@@ -456,7 +454,7 @@
 		var pathElems []ast.Label
 
 		switch {
-		case flagPath.String(cmd) != "":
+		case len(flagPath.StringArray(cmd)) > 0:
 			expr := expr
 			if flagWithContext.Bool(cmd) {
 				expr = ast.NewStruct(
@@ -471,25 +469,17 @@
 				return err
 			}
 
-			labels, err := parsePath(flagPath.String(cmd))
-			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.TemplateLabel:
-					return fmt.Errorf("template labels not supported in path flag")
+			for _, str := range flagPath.StringArray(cmd) {
+				l, err := parser.ParseExpr("<path flag>", str)
+				if err != nil {
+					return fmt.Errorf(`labels are of form "cue import -l foo -l 'strings.ToLower(bar)'": %v`, err)
 				}
+
+				str, err := inst.Eval(l).String()
+				if err != nil {
+					return fmt.Errorf("unsupported label path type: %v", err)
+				}
+				pathElems = append(pathElems, ast.NewString(str))
 			}
 		}
 
@@ -587,40 +577,6 @@
 	return idx
 }
 
-func parsePath(exprs string) (p []ast.Label, err error) {
-	f, err := parser.ParseFile("<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)]
diff --git a/cmd/cue/cmd/testdata/script/import_context.txt b/cmd/cue/cmd/testdata/script/import_context.txt
index b2a3802..34860ca 100644
--- a/cmd/cue/cmd/testdata/script/import_context.txt
+++ b/cmd/cue/cmd/testdata/script/import_context.txt
@@ -1,4 +1,4 @@
-cue import -o - -f --with-context -l '"\(path.Ext(filename)):\(index+1)/\(recordCount)" "\(data["@name"])"' ./import
+cue import -o - -f --with-context -l '"\(path.Ext(filename)):\(index+1)/\(recordCount)"' -l 'data["@name"]' ./import
 cmp stdout expect-stdout
 -- expect-stdout --
 ".jsonl:1/3": elem1: {
diff --git a/cmd/cue/cmd/testdata/script/import_hoiststr.txt b/cmd/cue/cmd/testdata/script/import_hoiststr.txt
index e8fda16..e0f50f6 100644
--- a/cmd/cue/cmd/testdata/script/import_hoiststr.txt
+++ b/cmd/cue/cmd/testdata/script/import_hoiststr.txt
@@ -1,4 +1,4 @@
-cue import -o - -f --list -l '"\(strings.ToLower(kind))" "\(name)"' --recursive ./import
+cue import -o - -f --list -l 'strings.ToLower(kind)' -l name --recursive ./import
 cmp stdout expect-stdout
 -- expect-stdout --
 import json656e63 "encoding/json"
diff --git a/cmd/cue/cmd/testdata/script/import_path.txt b/cmd/cue/cmd/testdata/script/import_path.txt
index 18aacdb..90ddb12 100644
--- a/cmd/cue/cmd/testdata/script/import_path.txt
+++ b/cmd/cue/cmd/testdata/script/import_path.txt
@@ -1,4 +1,4 @@
-cue import -o - -f -l '"\(strings.ToLower(kind))" "\(name)"' ./import
+cue import -o - -f -l 'strings.ToLower(kind)' -l name ./import
 cmp stdout expect-stdout
 -- expect-stdout --
 service: booster: {
diff --git a/doc/tutorial/kubernetes/README.md b/doc/tutorial/kubernetes/README.md
index 368eaed..8ad05ce 100644
--- a/doc/tutorial/kubernetes/README.md
+++ b/doc/tutorial/kubernetes/README.md
@@ -109,7 +109,7 @@
 tree at the path with the "kind" as first element and "name" as second.
 
 ```
-$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))": "\(metadata.name)"' -f
+$ cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f
 ```
 
 The added `-l` flag defines the labels for each object, based on values from
@@ -165,7 +165,7 @@
 <-- TODO: update import label format -->
 
 ```
-$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))": "\(metadata.name)"' -f -R
+$ cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f -R
 ```
 
 Now the file looks like: