cmd/cue/cmd: add --with-context flag for import

This is relevant for computing labels.
Instead of evaluating labels within the context
of an imported file, it evaluates it within a struct
wrapper where the file contents are assigned to
data, while other contextual information, like
filename is added.

Fixes #193

Change-Id: Iad8119c86c8a64ba1c8f071c87970d4acdf87a0e
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4904
Reviewed-by: roger peppe <rogpeppe@gmail.com>
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
index fdf4164..ec6f45a 100644
--- a/cmd/cue/cmd/import.go
+++ b/cmd/cue/cmd/import.go
@@ -21,6 +21,7 @@
 	"os"
 	"path/filepath"
 	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 	"unicode"
@@ -70,7 +71,7 @@
   $ cue import ./... -type=json
 
 
-The -path flag
+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.
@@ -78,15 +79,32 @@
 evaluated within the context of the imported file. label expressions may also
 refer to builtin packages, which will be implicitly imported.
 
+The --with-context flag can be used to evaluate the label expression within
+a struct with the following fields:
+
+{
+	// data holds the original source data
+	// (perhaps one of several records in a file).
+	data: _
+	// filename holds the full path to the file.
+	filename: string
+	// index holds the 0-based index element of the
+	// record within the file. For files containing only
+	// one record, this will be 0.
+	index: uint & <recordCount
+	// recordCount holds the total number of records
+	// within the file.
+	recordCount: int & >=1
+}
 
 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
+--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
+--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.
 
@@ -135,7 +153,15 @@
       name: "booster"
   }
 
-  deployment: booster: {
+  # base the path values on the input and file name
+  $ cue import -f --with-context -l '"\(path.Base(filename))" "\(data.kind)"' foo.yaml
+  $ cat foo.cue
+  "foo.yaml": Service: {
+      kind: "Service"
+      name: "booster"
+  }
+
+  "foo.yaml": Deployment: {
       kind:     "Deployment"
       name:     "booster
       replicas: 1
@@ -208,6 +234,7 @@
 	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")
+	cmd.Flags().Bool(string(flagWithContext), false, "import as object with contextual data")
 
 	cmd.Flags().String("fix", "", "apply given fix")
 
@@ -217,8 +244,9 @@
 }
 
 const (
-	flagFiles     flagName = "files"
-	flagProtoPath flagName = "proto_path"
+	flagFiles       flagName = "files"
+	flagProtoPath   flagName = "proto_path"
+	flagWithContext flagName = "with-context"
 )
 
 type importStreamFunc func(path string, r io.Reader) ([]ast.Expr, error)
@@ -362,7 +390,7 @@
 func processStream(cmd *Command, pkg, filename string, objs []ast.Expr) error {
 	if flagFiles.Bool(cmd) {
 		for i, f := range objs {
-			err := combineExpressions(cmd, pkg, newName(filename, i), f)
+			err := combineExpressions(cmd, pkg, filename, i, f)
 			if err != nil {
 				return err
 			}
@@ -373,13 +401,15 @@
 			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...)
+	return combineExpressions(cmd, pkg, filename, 0, objs...)
 }
 
 // TODO: implement a more fine-grained approach.
 var mutex sync.Mutex
 
-func combineExpressions(cmd *Command, pkg, cueFile string, objs ...ast.Expr) error {
+func combineExpressions(cmd *Command, pkg, filename string, idx int, objs ...ast.Expr) error {
+	cueFile := newName(filename, idx)
+
 	mutex.Lock()
 	defer mutex.Unlock()
 
@@ -420,13 +450,22 @@
 	}
 
 	index := newIndex()
-	for _, expr := range objs {
+	for i, expr := range objs {
 
 		// Compute a path different from root.
 		var pathElems []ast.Label
 
 		switch {
 		case flagPath.String(cmd) != "":
+			expr := expr
+			if flagWithContext.Bool(cmd) {
+				expr = ast.NewStruct(
+					"data", expr,
+					"filename", ast.NewString(filename),
+					"index", ast.NewLit(token.INT, strconv.Itoa(i)),
+					"recordCount", ast.NewLit(token.INT, strconv.Itoa(len(objs))),
+				)
+			}
 			inst, err := runtime.CompileExpr(expr)
 			if err != nil {
 				return err
diff --git a/cmd/cue/cmd/testdata/script/import_context.txt b/cmd/cue/cmd/testdata/script/import_context.txt
new file mode 100644
index 0000000..b2a3802
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/import_context.txt
@@ -0,0 +1,29 @@
+cue import -o - -f --with-context -l '"\(path.Ext(filename)):\(index+1)/\(recordCount)" "\(data["@name"])"' ./import
+cmp stdout expect-stdout
+-- expect-stdout --
+".jsonl:1/3": elem1: {
+	kind:    "Service"
+	"@name": "elem1"
+}
+".jsonl:2/3": elem2: {
+	kind:    "Deployment"
+	"@name": "elem2"
+}
+".jsonl:3/3": elem3: {
+	kind:    "Service"
+	"@name": "elem3"
+}
+-- import/services.jsonl --
+{
+    "kind": "Service",
+    "@name": "elem1"
+}
+{
+    "kind": "Deployment",
+    "@name": "elem2"
+}
+{
+    "kind": "Service",
+    "@name": "elem3"
+}
+-- cue.mod --