cmd/cue/cmd: allow capturing output

Issue #50

Change-Id: I06be2f1e4dc4ce1d531ddb9c35eb3c11c5693f8d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2162
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 a92d869..5defa9f 100644
--- a/cmd/cue/cmd/cmd_test.go
+++ b/cmd/cue/cmd/cmd_test.go
@@ -36,7 +36,7 @@
 		stderr = os.Stderr
 	}()
 	for _, name := range testCases {
-		rootCmd := newRootCmd()
+		rootCmd := newRootCmd().root
 		run := func(cmd *cobra.Command, args []string) error {
 			stdout = cmd.OutOrStdout()
 			stderr = cmd.OutOrStderr()
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
index 9878325..6f74ced 100644
--- a/cmd/cue/cmd/common.go
+++ b/cmd/cue/cmd/common.go
@@ -29,21 +29,17 @@
 	"github.com/spf13/cobra"
 )
 
-func init() {
-	s, err := os.Getwd()
-	if err == nil {
-		cwd = s
-	}
-}
-
 var runtime = &cue.Runtime{}
 
-var cwd = "////"
-
 // printHeader is a hacky and unprincipled way to sanatize the package path.
-func printHeader(w io.Writer, dir string) {
-	head := strings.Replace(dir, cwd, ".", 1)
-	fmt.Fprintf(w, "--- %s\n", head)
+func printHeader(w io.Writer, cwd, dir string) {
+	if cwd != "" {
+		if dir == cwd {
+			return
+		}
+		dir = strings.Replace(dir, cwd, ".", 1)
+	}
+	fmt.Fprintf(w, "--- %s\n", dir)
 }
 
 func exitIfErr(cmd *cobra.Command, inst *cue.Instance, err error, fatal bool) {
@@ -51,18 +47,24 @@
 }
 
 func exitOnErr(cmd *cobra.Command, file string, err error, fatal bool) {
-	if err != nil {
-		w := &bytes.Buffer{}
-		printHeader(w, file)
-		errors.Print(w, err)
+	if err == nil {
+		return
+	}
+	cwd := "////"
+	if p, _ := os.Getwd(); p != "" {
+		cwd = p
+	}
 
-		// TODO: do something more principled than this.
-		b := w.Bytes()
-		b = bytes.ReplaceAll(b, []byte(cwd), []byte("."))
-		cmd.OutOrStderr().Write(b)
-		if fatal {
-			exit()
-		}
+	w := &bytes.Buffer{}
+	printHeader(w, cwd, file)
+	errors.Print(w, err)
+
+	// TODO: do something more principled than this.
+	b := w.Bytes()
+	b = bytes.ReplaceAll(b, []byte(cwd), []byte("."))
+	cmd.OutOrStderr().Write(b)
+	if fatal {
+		exit()
 	}
 }
 
diff --git a/cmd/cue/cmd/root.go b/cmd/cue/cmd/root.go
index 164ad8e..59b262f 100644
--- a/cmd/cue/cmd/root.go
+++ b/cmd/cue/cmd/root.go
@@ -18,6 +18,7 @@
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	logger "log"
 	"os"
 
@@ -43,7 +44,7 @@
 var cfgFile string
 
 // newRootCmd creates the base command when called without any subcommands
-func newRootCmd() *cobra.Command {
+func newRootCmd() *Command {
 	cmd := &cobra.Command{
 		Use:   "cue",
 		Short: "cue emits configuration files to user-defined commands.",
@@ -70,6 +71,8 @@
 		SilenceUsage: true,
 	}
 
+	cmdCmd := newCmdCmd()
+
 	subCommands := []*cobra.Command{
 		newTrimCmd(),
 		newImportCmd(),
@@ -77,7 +80,7 @@
 		newGetCmd(),
 		newFmtCmd(),
 		newExportCmd(),
-		newCmdCmd(),
+		cmdCmd,
 		newVetCmd(),
 		newAddCmd(),
 	}
@@ -88,28 +91,57 @@
 		cmd.AddCommand(sub)
 	}
 
-	return cmd
+	return &Command{root: cmd, cmd: cmdCmd}
 }
 
 // Main runs the cue tool. It loads the tool flags.
 func Main(ctx context.Context, args []string) (err error) {
+	cmd, err := New(args)
+	if err != nil {
+		return err
+	}
+	return cmd.Run(ctx)
+}
+
+type Command struct {
+	root *cobra.Command
+
+	// Subcommands
+	cmd *cobra.Command
+}
+
+func (c *Command) SetOutput(w io.Writer) {
+	c.root.SetOutput(w)
+}
+
+func (c *Command) Run(ctx context.Context) (err error) {
 	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 e := recover().(type) {
-		case nil:
-		case panicError:
-			err = e.Err
-		default:
-			panic(err)
-		}
-		// We use panic to escape, instead of os.Exit
-	}()
-	rootCmd := newRootCmd()
+	defer recoverError(&err)
+
+	return c.root.Execute()
+}
+
+func recoverError(err *error) {
+	switch e := recover().(type) {
+	case nil:
+	case panicError:
+		*err = e.Err
+	default:
+		panic(e)
+	}
+	// We use panic to escape, instead of os.Exit
+}
+
+func New(args []string) (cmd *Command, err error) {
+	defer recoverError(&err)
+
+	cmd = newRootCmd()
+	rootCmd := cmd.root
 	rootCmd.SetArgs(args)
 	if len(args) >= 1 && args[0] != "help" {
 		// TODO: for now we only allow one instance. Eventually, we can allow
@@ -125,7 +157,7 @@
 			cmd  *cobra.Command
 		}
 		sub := map[string]subSpec{
-			"cmd": {commandSection, newCmdCmd()},
+			"cmd": {commandSection, cmd.cmd},
 			// "serve": {"server", nil},
 			// "fix":   {"fix", nil},
 		}
@@ -137,12 +169,12 @@
 				commands := tools.Lookup(sub.name)
 				i, err := commands.Fields()
 				if err != nil {
-					return err
+					return nil, err
 				}
 				for i.Next() {
 					_, _ = addCustom(sub.cmd, sub.name, i.Label(), tools)
 				}
-				return nil
+				return cmd, nil
 			}
 			tools := buildTools(rootCmd, args[1:])
 			_, err := addCustom(sub.cmd, sub.name, args[0], tools)
@@ -152,7 +184,7 @@
 			}
 		}
 	}
-	return rootCmd.Execute()
+	return cmd, nil
 }
 
 type panicError struct {
diff --git a/cmd/cue/cmd/vet.go b/cmd/cue/cmd/vet.go
index b4175df..962743f 100644
--- a/cmd/cue/cmd/vet.go
+++ b/cmd/cue/cmd/vet.go
@@ -15,6 +15,8 @@
 package cmd
 
 import (
+	"os"
+
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/parser"
@@ -44,6 +46,8 @@
 
 	w := cmd.OutOrStdout()
 
+	cwd, _ := os.Getwd()
+
 	for _, inst := range instances {
 		// TODO: use ImportPath or some other sanitized path.
 		opt := []cue.Option{
@@ -54,7 +58,7 @@
 		}
 		err := inst.Value().Validate(opt...)
 		if flagVerbose.Bool(cmd) || err != nil {
-			printHeader(w, inst.Dir)
+			printHeader(w, cwd, inst.Dir)
 		}
 		exitIfErr(cmd, inst, err, false)
 	}
diff --git a/cmd/cue/main.go b/cmd/cue/main.go
index 4c7450f..d319567 100644
--- a/cmd/cue/main.go
+++ b/cmd/cue/main.go
@@ -16,7 +16,7 @@
 
 import (
 	"context"
-	"log"
+	"fmt"
 	"os"
 
 	"cuelang.org/go/cmd/cue/cmd"
@@ -25,7 +25,7 @@
 func main() {
 	err := cmd.Main(context.Background(), os.Args[1:])
 	if err != nil {
-		log.Println(err)
+		fmt.Println(err)
 		os.Exit(1)
 	}
 }