erros/cue: allow OS-independent error messages

Mostly for testing purposes.

- Strip cwd (more principled than before)
- Convert paths ToSlash in test mode

Change-Id: I24dfdd5b3157df0fe14c1934f559bdfbcd9c353a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2260
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/cmd_test.go b/cmd/cue/cmd/cmd_test.go
index e892730..0ad23ff 100644
--- a/cmd/cue/cmd/cmd_test.go
+++ b/cmd/cue/cmd/cmd_test.go
@@ -23,6 +23,8 @@
 )
 
 func TestCmd(t *testing.T) {
+	cfg := printConfig(t)
+
 	testCases := []string{
 		"echo",
 		"run",
@@ -48,7 +50,7 @@
 			}
 			err = executeTasks("command", name, tools)
 			if err != nil {
-				errors.Print(stdout, err, nil)
+				errors.Print(stdout, err, cfg)
 			}
 			return nil
 		}
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
index 3ed7383..b2315cc 100644
--- a/cmd/cue/cmd/common.go
+++ b/cmd/cue/cmd/common.go
@@ -32,6 +32,8 @@
 
 var runtime = &cue.Runtime{}
 
+var inTest = false
+
 func exitIfErr(cmd *cobra.Command, inst *cue.Instance, err error, fatal bool) {
 	exitOnErr(cmd, err, fatal)
 }
@@ -40,10 +42,6 @@
 	if err == nil {
 		return
 	}
-	cwd := "////"
-	if p, _ := os.Getwd(); p != "" {
-		cwd = p
-	}
 
 	// Link x/text as our localizer.
 	lang, _ := jibber_jabber.DetectIETF()
@@ -52,12 +50,16 @@
 		p.Fprintf(w, format, args...)
 	}
 
-	w := &bytes.Buffer{}
-	errors.Print(w, err, &errors.Config{Format: format})
+	cwd, _ := os.Getwd()
 
-	// TODO: do something more principled than this.
+	w := &bytes.Buffer{}
+	errors.Print(w, err, &errors.Config{
+		Format:  format,
+		Cwd:     cwd,
+		ToSlash: inTest,
+	})
+
 	b := w.Bytes()
-	b = bytes.ReplaceAll(b, []byte(cwd), []byte("."))
 	cmd.OutOrStderr().Write(b)
 	if fatal {
 		exit()
diff --git a/cmd/cue/cmd/common_test.go b/cmd/cue/cmd/common_test.go
index 6a44a37..49b6f87 100644
--- a/cmd/cue/cmd/common_test.go
+++ b/cmd/cue/cmd/common_test.go
@@ -15,14 +15,12 @@
 package cmd
 
 import (
-	"bytes"
 	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
 	"testing"
 
 	"cuelang.org/go/cue/errors"
@@ -34,16 +32,30 @@
 
 var update = flag.Bool("update", false, "update the test files")
 
+func printConfig(t *testing.T) *errors.Config {
+	t.Helper()
+
+	inTest = true
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return &errors.Config{
+		Cwd:     cwd,
+		ToSlash: true,
+	}
+}
+
 func runCommand(t *testing.T, cmd *cobra.Command, name string, args ...string) {
 	t.Helper()
 	log.SetFlags(0)
 
-	cwd, err := os.Getwd()
-	if err != nil {
-		log.Fatal(err)
-	}
 	const dir = "./testdata"
 
+	cfg := printConfig(t)
+
 	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
 		t.Run(path, func(t *testing.T) {
 			if err != nil {
@@ -72,9 +84,9 @@
 					switch err := recover().(type) {
 					case nil:
 					case panicError:
-						errors.Print(wOut, err.Err, nil)
+						errors.Print(wOut, err.Err, cfg)
 					case error:
-						errors.Print(wOut, err, nil)
+						errors.Print(wOut, err, cfg)
 					default:
 						fmt.Fprintln(wOut, err)
 					}
@@ -89,9 +101,6 @@
 			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
diff --git a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
index 753329f..39304d4 100644
--- a/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
+++ b/cmd/cue/cmd/testdata/tasks/cmd_baddisplay.out
@@ -1,3 +1,3 @@
 unsupported op &(int, string):
-    $CWD/testdata/tasks/task_tool.cue:29:9
+    ./testdata/tasks/task_tool.cue:29:9
     tool/cli:4:9
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index e5e1a72..9111e65 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -23,6 +23,7 @@
 	"errors"
 	"fmt"
 	"io"
+	"path/filepath"
 	"sort"
 	"strings"
 
@@ -335,6 +336,13 @@
 	// Format formats the given string and arguments and writes it to w.
 	// It is used for all printing.
 	Format func(w io.Writer, format string, args ...interface{})
+
+	// Cwd is the current working directory. Filename positions are taken
+	// relative to this path.
+	Cwd string
+
+	// ToSlash sets whether to use Unix paths. Mostly used for testing.
+	ToSlash bool
 }
 
 // Print is a utility function that prints a list of errors to w,
@@ -369,7 +377,32 @@
 
 	positions := []string{}
 	for _, p := range Positions(err) {
-		positions = append(positions, p.String())
+		pos := p.Position()
+		s := pos.Filename
+		if cfg.Cwd != "" {
+			if p, err := filepath.Rel(cfg.Cwd, s); err == nil {
+				s = p
+				// Some IDEs (e.g. VSCode) only recognize a path if it start
+				// with a dot. This also helps to distinguish between local
+				// files and builtin packages.
+				if !strings.HasPrefix(s, ".") {
+					s = fmt.Sprintf(".%s%s", string(filepath.Separator), s)
+				}
+			}
+		}
+		if cfg.ToSlash {
+			s = filepath.ToSlash(s)
+		}
+		if pos.IsValid() {
+			if s != "" {
+				s += ":"
+			}
+			s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
+		}
+		if s == "" {
+			s = "-"
+		}
+		positions = append(positions, s)
 	}
 
 	if path := Path(err); path != nil {