lib: gosh: switch from Logf/Fatalf to TB, and related

This change makes most of the changes described in the final
proposal in https://v.io/i/1128 - namely, it makes all the
requisite API changes.

Things not done in this change:
- "Good PC" logging
- Printing Cmd stdout/stderr on failure

Those will be done in subsequent changes.

MultiPart: 4/9

Change-Id: I04b0fa0af8aa358a42db055a9c8503ffa4e88904
diff --git a/gosh/.api b/gosh/.api
index 10ee7c1..0dd3f73 100644
--- a/gosh/.api
+++ b/gosh/.api
@@ -2,7 +2,7 @@
 pkg gosh, func InitChildMain()
 pkg gosh, func InitMain()
 pkg gosh, func NewPipeline(*Cmd, ...*Cmd) *Pipeline
-pkg gosh, func NewShell(Opts) *Shell
+pkg gosh, func NewShell(TB) *Shell
 pkg gosh, func RegisterFunc(string, interface{}) *Func
 pkg gosh, func SendVars(map[string]string)
 pkg gosh, method (*Cmd) AddStderrWriter(io.Writer)
@@ -61,14 +61,14 @@
 pkg gosh, type Cmd struct, PropagateOutput bool
 pkg gosh, type Cmd struct, Vars map[string]string
 pkg gosh, type Func struct
-pkg gosh, type Opts struct
-pkg gosh, type Opts struct, ChildOutputDir string
-pkg gosh, type Opts struct, Fatalf func(string, ...interface{})
-pkg gosh, type Opts struct, Logf func(string, ...interface{})
-pkg gosh, type Opts struct, PropagateChildOutput bool
 pkg gosh, type Pipeline struct
 pkg gosh, type Shell struct
 pkg gosh, type Shell struct, Args []string
+pkg gosh, type Shell struct, ChildOutputDir string
+pkg gosh, type Shell struct, ContinueOnError bool
 pkg gosh, type Shell struct, Err error
-pkg gosh, type Shell struct, Opts Opts
+pkg gosh, type Shell struct, PropagateChildOutput bool
 pkg gosh, type Shell struct, Vars map[string]string
+pkg gosh, type TB interface { FailNow, Logf }
+pkg gosh, type TB interface, FailNow()
+pkg gosh, type TB interface, Logf(string, ...interface{})
diff --git a/gosh/cmd.go b/gosh/cmd.go
index 9357094..e72b4d6 100644
--- a/gosh/cmd.go
+++ b/gosh/cmd.go
@@ -50,9 +50,9 @@
 	// the given duration has elapsed. Only takes effect if the child process was
 	// spawned via Shell.FuncCmd or explicitly calls InitChildMain.
 	ExitAfter time.Duration
-	// PropagateOutput is inherited from Shell.Opts.PropagateChildOutput.
+	// PropagateOutput is inherited from Shell.PropagateChildOutput.
 	PropagateOutput bool
-	// OutputDir is inherited from Shell.Opts.ChildOutputDir.
+	// OutputDir is inherited from Shell.ChildOutputDir.
 	OutputDir string
 	// ExitErrorIsOk specifies whether an *exec.ExitError should be reported via
 	// Shell.HandleError.
@@ -446,12 +446,12 @@
 	case c.c.Stdin != nil:
 		return nil, errAlreadySetStdin
 	}
-	// We want to provide an unlimited-size pipe to the user. If we set
-	// c.c.Stdin directly to the newBufferedPipe, the os/exec package will
-	// create an os.Pipe for us, along with a goroutine to copy data over. And
-	// exec.Cmd.Wait will wait for this goroutine to exit before returning, even
-	// if the process has already exited. That means the user will be forced to
-	// call Close on the returned WriteCloser, which is annoying.
+	// We want to provide an unlimited-size pipe to the user. If we set c.c.Stdin
+	// directly to the newBufferedPipe, the os/exec package will create an os.Pipe
+	// for us, along with a goroutine to copy data over. And exec.Cmd.Wait will
+	// wait for this goroutine to exit before returning, even if the process has
+	// already exited. That means the user will be forced to call Close on the
+	// returned WriteCloser, which is annoying.
 	//
 	// Instead, we set c.c.Stdin to our own os.Pipe, so that os/exec won't create
 	// the pipe nor the goroutine. We chain our newBufferedPipe in front of this,
diff --git a/gosh/internal/gosh_example/main.go b/gosh/internal/gosh_example/main.go
index a3072ad..18d00f4 100644
--- a/gosh/internal/gosh_example/main.go
+++ b/gosh/internal/gosh_example/main.go
@@ -13,7 +13,7 @@
 
 // Mirrors TestCmd in shell_test.go.
 func ExampleCmd() {
-	sh := gosh.NewShell(gosh.Opts{})
+	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
 	// Start server.
@@ -37,7 +37,7 @@
 
 // Mirrors TestFuncCmd in shell_test.go.
 func ExampleFuncCmd() {
-	sh := gosh.NewShell(gosh.Opts{})
+	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
 	// Start server.
diff --git a/gosh/pipeline.go b/gosh/pipeline.go
index c9aec89..296f2eb 100644
--- a/gosh/pipeline.go
+++ b/gosh/pipeline.go
@@ -163,15 +163,15 @@
 
 // handleError is used instead of direct calls to Shell.HandleError throughout
 // the pipeline implementation. This is needed to handle the case where the user
-// has set Shell.Opts.Fatalf to a non-fatal function.
+// has set Shell.ContinueOnError to true.
 //
 // The general pattern is that after each Shell or Cmd method is called, we
-// check p.sh.Err. If there was an error it is wrapped with errAlreadyHandled,
-// indicating that Shell.HandleError has already been called with this error,
-// and should not be called again.
+// check p.sh.Err; if it's non-nil, we wrap it with errAlreadyHandled to
+// indicate that Shell.HandleError has already been called with this error and
+// should not be called again.
 func handleError(sh *Shell, err error) {
 	if _, ok := err.(errAlreadyHandled); ok {
-		return // the shell already handled this error
+		return // the shell has already handled this error
 	}
 	sh.HandleError(err)
 }
@@ -253,12 +253,12 @@
 }
 
 // TODO(toddw): Clean up resources in Shell.Cleanup. E.g. we'll currently leak
-// the os.Pipe fds if the user sets up a pipeline, but never calls Start (or
+// the os.Pipe fds if the user sets up a pipeline but never calls Start (or
 // Wait, Terminate).
 
 func (p *Pipeline) start() error {
 	// Start all commands in the pipeline, capturing the first error.
-	// Ensure all commands are processed, by avoiding early-exit.
+	// Ensure all commands are processed by avoiding early-exit.
 	var shErr, closeErr error
 	for i, c := range p.cmds {
 		p.sh.Err = nil
@@ -288,7 +288,7 @@
 
 func (p *Pipeline) wait() error {
 	// Wait for all commands in the pipeline, capturing the first error.
-	// Ensure all commands are processed, by avoiding early-exit.
+	// Ensure all commands are processed by avoiding early-exit.
 	var shErr, closeErr error
 	for i, c := range p.cmds {
 		p.sh.Err = nil
diff --git a/gosh/pipeline_test.go b/gosh/pipeline_test.go
index 25901e2..95dbf4a 100644
--- a/gosh/pipeline_test.go
+++ b/gosh/pipeline_test.go
@@ -14,7 +14,7 @@
 )
 
 func TestPipeline(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	echo := sh.FuncCmd(echoFunc)
@@ -55,9 +55,9 @@
 }
 
 func TestPipelineDifferentShells(t *testing.T) {
-	sh1 := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh1 := gosh.NewShell(t)
 	defer sh1.Cleanup()
-	sh2 := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh2 := gosh.NewShell(t)
 	defer sh2.Cleanup()
 
 	setsErr(t, sh1, func() { gosh.NewPipeline(sh1.FuncCmd(echoFunc), sh2.FuncCmd(catFunc)) })
@@ -71,7 +71,7 @@
 }
 
 func TestPipelineClosedPipe(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 	writeLoop, readLine := sh.FuncCmd(writeLoopFunc), sh.FuncCmd(readFunc)
 
@@ -88,7 +88,7 @@
 }
 
 func TestPipelineCmdFailure(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 	cat := sh.FuncCmd(catFunc)
 	exit1 := sh.FuncCmd(exitFunc, 1)
@@ -170,7 +170,7 @@
 }
 
 func TestPipelineSignal(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	for _, d := range []time.Duration{0, time.Hour} {
@@ -204,7 +204,7 @@
 }
 
 func TestPipelineTerminate(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	for _, d := range []time.Duration{0, time.Hour} {
diff --git a/gosh/registry.go b/gosh/registry.go
index 64453a8..2202722 100644
--- a/gosh/registry.go
+++ b/gosh/registry.go
@@ -51,7 +51,7 @@
 	// Register the function's args with gob. Needed because Shell.Func takes
 	// interface{} arguments.
 	for i := 0; i < t.NumIn(); i++ {
-		// Note: Clients are responsible for registering any concrete types stored
+		// Note: Users are responsible for registering any concrete types stored
 		// inside interface{} arguments.
 		if t.In(i).Kind() == reflect.Interface {
 			continue
@@ -92,7 +92,7 @@
 		if arg != nil {
 			av = reflect.ValueOf(arg)
 		} else {
-			// Client passed nil; construct the zero value for this argument based on
+			// User passed nil; construct the zero value for this argument based on
 			// the function signature.
 			av = reflect.Zero(argType(t, i))
 		}
diff --git a/gosh/shell.go b/gosh/shell.go
index c318b57..a42d69c 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -24,16 +24,16 @@
 	"os/signal"
 	"path"
 	"path/filepath"
+	"runtime/debug"
 	"sync"
 	"syscall"
 	"time"
 )
 
 const (
-	envChildOutputDir = "GOSH_CHILD_OUTPUT_DIR"
-	envExitAfter      = "GOSH_EXIT_AFTER"
-	envInvocation     = "GOSH_INVOCATION"
-	envWatchParent    = "GOSH_WATCH_PARENT"
+	envExitAfter   = "GOSH_EXIT_AFTER"
+	envInvocation  = "GOSH_INVOCATION"
+	envWatchParent = "GOSH_WATCH_PARENT"
 )
 
 var (
@@ -42,19 +42,35 @@
 	errDidNotCallNewShell   = errors.New("gosh: did not call gosh.NewShell")
 )
 
+// TB is a subset of the testing.TB interface, defined here to avoid depending
+// on the testing package.
+type TB interface {
+	FailNow()
+	Logf(format string, args ...interface{})
+}
+
 // Shell represents a shell. Not thread-safe.
 type Shell struct {
-	// Err is the most recent error from this Shell or any of its Cmds (may be
-	// nil).
+	// Err is the most recent error from this Shell or any of its child Cmds (may
+	// be nil).
 	Err error
-	// Opts is the Opts struct for this Shell, with default values filled in.
-	Opts Opts
+	// PropagateChildOutput specifies whether to propagate child stdout and stderr
+	// up to the parent's stdout and stderr.
+	PropagateChildOutput bool
+	// ChildOutputDir, if non-empty, makes it so child stdout and stderr are tee'd
+	// to files in the specified directory.
+	ChildOutputDir string
+	// ContinueOnError specifies whether to invoke TB.FailNow on error, i.e.
+	// whether to panic on error. Users that set ContinueOnError to true should
+	// inspect sh.Err after each Shell method invocation.
+	ContinueOnError bool
 	// Vars is the map of env vars for this Shell.
 	Vars map[string]string
 	// Args is the list of args to append to subsequent command invocations.
 	Args []string
 	// Internal state.
 	calledNewShell  bool
+	tb              TB
 	cleanupDone     chan struct{}
 	cleanupMu       sync.Mutex // protects the fields below; held during cleanup
 	calledCleanup   bool
@@ -65,36 +81,24 @@
 	cleanupHandlers []func()
 }
 
-// Opts configures Shell.
-type Opts struct {
-	// Fatalf is called whenever an error is encountered.
-	// If not specified, defaults to panic(fmt.Sprintf(format, v...)).
-	Fatalf func(format string, v ...interface{})
-	// Logf is called to log things.
-	// If not specified, defaults to log.Printf(format, v...).
-	Logf func(format string, v ...interface{})
-	// Child stdout and stderr are propagated up to the parent's stdout and stderr
-	// iff PropagateChildOutput is true.
-	PropagateChildOutput bool
-	// If specified, each child's stdout and stderr streams are also piped to
-	// files in this directory.
-	// If not specified, defaults to GOSH_CHILD_OUTPUT_DIR.
-	ChildOutputDir string
-}
-
-// NewShell returns a new Shell.
-func NewShell(opts Opts) *Shell {
-	sh, err := newShell(opts)
+// NewShell returns a new Shell. Tests and benchmarks should pass their
+// testing.TB instance; non-tests should pass nil.
+func NewShell(tb TB) *Shell {
+	sh, err := newShell(tb)
 	sh.HandleError(err)
 	return sh
 }
 
-// HandleError sets sh.Err. If err is not nil, it also calls sh.Opts.Fatalf.
+// HandleError sets sh.Err. If err is not nil and sh.ContinueOnError is false,
+// it also calls TB.FailNow.
 func (sh *Shell) HandleError(err error) {
 	sh.Ok()
 	sh.Err = err
-	if err != nil && sh.Opts.Fatalf != nil {
-		sh.Opts.Fatalf("%v", err)
+	if err != nil && !sh.ContinueOnError {
+		if sh.tb != pkgLevelDefaultTB {
+			sh.tb.Logf(string(debug.Stack()))
+		}
+		sh.tb.FailNow()
 	}
 }
 
@@ -204,32 +208,31 @@
 ////////////////////////////////////////
 // Internals
 
-// Note: On error, newShell returns a *Shell with Opts.Fatalf initialized to
-// simplify things for the caller.
-func newShell(opts Opts) (*Shell, error) {
-	osVars := sliceToMap(os.Environ())
-	if opts.Fatalf == nil {
-		opts.Fatalf = func(format string, v ...interface{}) {
-			panic(fmt.Sprintf(format, v...))
-		}
-	}
-	if opts.Logf == nil {
-		opts.Logf = func(format string, v ...interface{}) {
-			log.Printf(format, v...)
-		}
-	}
-	if opts.ChildOutputDir == "" {
-		opts.ChildOutputDir = osVars[envChildOutputDir]
+type defaultTB struct{}
+
+func (*defaultTB) FailNow() {
+	panic(nil)
+}
+
+func (*defaultTB) Logf(format string, args ...interface{}) {
+	log.Printf(format, args...)
+}
+
+var pkgLevelDefaultTB *defaultTB = &defaultTB{}
+
+func newShell(tb TB) (*Shell, error) {
+	if tb == nil {
+		tb = pkgLevelDefaultTB
 	}
 	// Filter out any gosh env vars coming from outside.
-	shVars := copyMap(osVars)
-	for _, key := range []string{envChildOutputDir, envExitAfter, envInvocation, envWatchParent} {
+	shVars := sliceToMap(os.Environ())
+	for _, key := range []string{envExitAfter, envInvocation, envWatchParent} {
 		delete(shVars, key)
 	}
 	sh := &Shell{
-		Opts:           opts,
 		Vars:           shVars,
 		calledNewShell: true,
+		tb:             tb,
 		cleanupDone:    make(chan struct{}),
 	}
 	sh.cleanupOnSignal()
@@ -245,7 +248,7 @@
 		select {
 		case sig := <-ch:
 			// A termination signal was received; the process will exit.
-			sh.logf("Received signal: %v\n", sig)
+			sh.tb.Logf("Received signal: %v\n", sig)
 			sh.cleanupMu.Lock()
 			defer sh.cleanupMu.Unlock()
 			if !sh.calledCleanup {
@@ -262,12 +265,6 @@
 	}()
 }
 
-func (sh *Shell) logf(format string, v ...interface{}) {
-	if sh.Opts.Logf != nil {
-		sh.Opts.Logf(format, v...)
-	}
-}
-
 func (sh *Shell) cmd(vars map[string]string, name string, args ...string) (*Cmd, error) {
 	if vars == nil {
 		vars = make(map[string]string)
@@ -276,8 +273,8 @@
 	if err != nil {
 		return nil, err
 	}
-	c.PropagateOutput = sh.Opts.PropagateChildOutput
-	c.OutputDir = sh.Opts.ChildOutputDir
+	c.PropagateOutput = sh.PropagateChildOutput
+	c.OutputDir = sh.ChildOutputDir
 	return c, nil
 }
 
@@ -313,7 +310,7 @@
 			continue
 		}
 		if err := c.wait(); !c.errorIsOk(err) {
-			sh.logf("%s (PID %d) failed: %v\n", c.Path, c.Pid(), err)
+			sh.tb.Logf("%s (PID %d) failed: %v\n", c.Path, c.Pid(), err)
 			res = err
 		}
 	}
@@ -454,14 +451,14 @@
 	// Send os.Interrupt first; if that doesn't work, send os.Kill.
 	anyRunning := sh.forEachRunningCmd(func(c *Cmd) {
 		if err := c.signal(os.Interrupt); err != nil {
-			sh.logf("%d.Signal(os.Interrupt) failed: %v\n", c.Pid(), err)
+			sh.tb.Logf("%d.Signal(os.Interrupt) failed: %v\n", c.Pid(), err)
 		}
 	})
 	// If any child is still running, wait for 100ms.
 	if anyRunning {
 		time.Sleep(100 * time.Millisecond)
 		anyRunning = sh.forEachRunningCmd(func(c *Cmd) {
-			sh.logf("%s (PID %d) did not die\n", c.Path, c.Pid())
+			sh.tb.Logf("%s (PID %d) did not die\n", c.Path, c.Pid())
 		})
 	}
 	// If any child is still running, wait for another second, then send os.Kill
@@ -470,10 +467,10 @@
 		time.Sleep(time.Second)
 		sh.forEachRunningCmd(func(c *Cmd) {
 			if err := c.signal(os.Kill); err != nil {
-				sh.logf("%d.Signal(os.Kill) failed: %v\n", c.Pid(), err)
+				sh.tb.Logf("%d.Signal(os.Kill) failed: %v\n", c.Pid(), err)
 			}
 		})
-		sh.logf("Killed all remaining child processes\n")
+		sh.tb.Logf("Killed all remaining child processes\n")
 	}
 }
 
@@ -485,23 +482,23 @@
 	for _, tempFile := range sh.tempFiles {
 		name := tempFile.Name()
 		if err := tempFile.Close(); err != nil {
-			sh.logf("%q.Close() failed: %v\n", name, err)
+			sh.tb.Logf("%q.Close() failed: %v\n", name, err)
 		}
 		if err := os.RemoveAll(name); err != nil {
-			sh.logf("os.RemoveAll(%q) failed: %v\n", name, err)
+			sh.tb.Logf("os.RemoveAll(%q) failed: %v\n", name, err)
 		}
 	}
 	// Delete all temporary directories.
 	for _, tempDir := range sh.tempDirs {
 		if err := os.RemoveAll(tempDir); err != nil {
-			sh.logf("os.RemoveAll(%q) failed: %v\n", tempDir, err)
+			sh.tb.Logf("os.RemoveAll(%q) failed: %v\n", tempDir, err)
 		}
 	}
 	// Change back to the top of the dir stack.
 	if len(sh.dirStack) > 0 {
 		dir := sh.dirStack[0]
 		if err := os.Chdir(dir); err != nil {
-			sh.logf("os.Chdir(%q) failed: %v\n", dir, err)
+			sh.tb.Logf("os.Chdir(%q) failed: %v\n", dir, err)
 		}
 	}
 	// Call cleanup handlers in LIFO order.
diff --git a/gosh/shell_test.go b/gosh/shell_test.go
index 1cb124f..00147c4 100644
--- a/gosh/shell_test.go
+++ b/gosh/shell_test.go
@@ -6,10 +6,10 @@
 
 // TODO(sadovsky): Add more tests:
 // - effects of Shell.Cleanup
-// - Shell.{Vars,Args,Rename,MakeTempFile,MakeTempDir}
-// - Shell.Opts.{PropagateChildOutput,ChildOutputDir}
+// - Shell.{PropagateChildOutput,ChildOutputDir,Vars,Args}
+// - Shell.{Move,MakeTempFile}
+// - Cmd.{IgnoreParentExit,ExitAfter,PropagateOutput}
 // - Cmd.Clone
-// - Cmd.Opts.{IgnoreParentExit,ExitAfter,PropagateOutput}
 
 import (
 	"bufio"
@@ -73,21 +73,13 @@
 	return string(b)
 }
 
-func makeFatalf(t *testing.T) func(string, ...interface{}) {
-	return func(format string, v ...interface{}) {
-		debug.PrintStack()
-		t.Fatalf(format, v...)
-	}
-}
-
 func setsErr(t *testing.T, sh *gosh.Shell, f func()) {
-	calledFatalf := false
-	sh.Opts.Fatalf = func(string, ...interface{}) { calledFatalf = true }
+	continueOnError := sh.ContinueOnError
+	sh.ContinueOnError = true
 	f()
 	nok(t, sh.Err)
-	eq(t, calledFatalf, true)
 	sh.Err = nil
-	sh.Opts.Fatalf = makeFatalf(t)
+	sh.ContinueOnError = continueOnError
 }
 
 ////////////////////////////////////////
@@ -138,20 +130,32 @@
 ////////////////////////////////////////
 // Tests
 
-func TestCustomFatalf(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+type customTB struct {
+	t             *testing.T
+	calledFailNow bool
+}
+
+func (tb *customTB) FailNow() {
+	tb.calledFailNow = true
+}
+
+func (tb *customTB) Logf(format string, args ...interface{}) {
+	tb.t.Logf(format, args...)
+}
+
+func TestCustomTB(t *testing.T) {
+	tb := &customTB{t: t}
+	sh := gosh.NewShell(tb)
 	defer sh.Cleanup()
 
-	var calledFatalf bool
-	sh.Opts.Fatalf = func(string, ...interface{}) { calledFatalf = true }
 	sh.HandleError(fakeError)
 	// Note, our deferred sh.Cleanup() should succeed despite this error.
 	nok(t, sh.Err)
-	eq(t, calledFatalf, true)
+	eq(t, tb.calledFailNow, true)
 }
 
 func TestPushdPopd(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	startDir, err := os.Getwd()
@@ -193,7 +197,7 @@
 
 func TestPushdNoPopdCleanup(t *testing.T) {
 	startDir := getwdEvalSymlinks(t)
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	tmpDir := sh.MakeTempDir()
 	sh.Pushd(tmpDir)
 	eq(t, getwdEvalSymlinks(t), evalSymlinks(t, tmpDir))
@@ -211,7 +215,7 @@
 
 // Mirrors ExampleCmd in internal/gosh_example/main.go.
 func TestCmd(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Start server.
@@ -235,7 +239,7 @@
 
 // Mirrors ExampleFuncCmd in internal/gosh_example/main.go.
 func TestFuncCmd(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Start server.
@@ -254,7 +258,7 @@
 	if testing.Short() {
 		t.Skip()
 	}
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Set -o to an absolute name.
@@ -297,7 +301,7 @@
 // Tests that Shell.Cmd uses Shell.Vars["PATH"] to locate executables with
 // relative names.
 func TestLookPath(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	binDir := sh.MakeTempDir()
@@ -326,7 +330,7 @@
 
 // Tests that AwaitVars works under various conditions.
 func TestAwaitVars(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	c := sh.FuncCmd(sendVarsFunc, map[string]string{"a": "1"})
@@ -371,7 +375,7 @@
 
 // Tests that AwaitVars returns immediately when the process exits.
 func TestAwaitProcessExit(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	c := sh.FuncCmd(exitFunc, 0)
@@ -399,7 +403,7 @@
 
 // Tests function signature-checking and execution.
 func TestRegistry(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Variadic functions. Non-variadic functions are sufficiently covered in
@@ -441,7 +445,7 @@
 }
 
 func TestStdin(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// The "cat" command exits after the reader returns EOF.
@@ -487,7 +491,7 @@
 }
 
 func TestStdinPipeWriteUntilExit(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Ensure that Write calls on stdin fail after the process exits. Note that we
@@ -532,7 +536,7 @@
 })
 
 func TestStdoutStderr(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Write to stdout only.
@@ -564,7 +568,7 @@
 }
 
 var writeMoreFunc = gosh.RegisterFunc("writeMoreFunc", func() {
-	sh := gosh.NewShell(gosh.Opts{})
+	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
 	c := sh.FuncCmd(writeFunc, true, true)
@@ -578,7 +582,7 @@
 
 // Tests that it's safe to add os.Stdout and os.Stderr as writers.
 func TestAddStdoutStderrWriter(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	stdout, stderr := sh.FuncCmd(writeMoreFunc).StdoutStderr()
@@ -587,7 +591,7 @@
 }
 
 func TestCombinedOutput(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	c := sh.FuncCmd(writeFunc, true, true)
@@ -604,7 +608,7 @@
 }
 
 func TestOutputDir(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	dir := sh.MakeTempDir()
@@ -645,7 +649,7 @@
 })
 
 func TestSignal(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	for _, d := range []time.Duration{0, time.Hour} {
@@ -677,7 +681,7 @@
 }
 
 func TestTerminate(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	for _, d := range []time.Duration{0, time.Hour} {
@@ -701,7 +705,7 @@
 }
 
 func TestShellWait(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	d0 := time.Duration(0)
@@ -745,7 +749,7 @@
 }
 
 func TestExitErrorIsOk(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Exit code 0 is not an error.
@@ -768,7 +772,7 @@
 }
 
 func TestIgnoreClosedPipeError(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
 	// Since writeLoopFunc will only finish if it receives a write error, it's
@@ -814,14 +818,16 @@
 		sh.Ok()
 	}()
 	func() { // errShellErrIsNotNil
-		sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
 		defer sh.Cleanup()
 		sh.Err = fakeError
 		defer func() { neq(t, recover(), nil) }()
 		sh.Ok()
 	}()
 	func() { // errAlreadyCalledCleanup
-		sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
 		sh.Cleanup()
 		defer func() { neq(t, recover(), nil) }()
 		sh.Ok()
@@ -836,14 +842,16 @@
 		sh.HandleError(fakeError)
 	}()
 	func() { // errShellErrIsNotNil
-		sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
 		defer sh.Cleanup()
 		sh.Err = fakeError
 		defer func() { neq(t, recover(), nil) }()
 		sh.HandleError(fakeError)
 	}()
 	func() { // errAlreadyCalledCleanup
-		sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
 		sh.Cleanup()
 		defer func() { neq(t, recover(), nil) }()
 		sh.HandleError(fakeError)
@@ -861,14 +869,14 @@
 
 // Tests that sh.Cleanup succeeds even if sh.Err is not nil.
 func TestCleanupAfterError(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	sh.Err = fakeError
 	sh.Cleanup()
 }
 
 // Tests that sh.Cleanup can be called multiple times.
 func TestMultipleCleanup(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	sh.Cleanup()
 	sh.Cleanup()
 }
diff --git a/vlog/flags_test.go b/vlog/flags_test.go
index f22ed59..707ac55 100644
--- a/vlog/flags_test.go
+++ b/vlog/flags_test.go
@@ -42,7 +42,7 @@
 })
 
 func TestFlags(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
+	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 	sh.FuncCmd(child).Run()
 }