lib: gosh: address TODOs

More specifically:
- Eliminates Shell.Main, since it's easy enough to use
Shell.Fn and set the returned Cmd's Args
- Renames Shell.Fn to Shell.FuncCmd
- Extends Shell.{Cmd,FuncCmd} comments to explain what's
done with the given arguments
- Makes it so NewShell takes a snapshot of os.Environ() and
uses that henceforth
- Makes it so NewShell filters out any gosh env vars coming
from outside
- Renames registry.go's Fn to Func, Register to
RegisterFunc, and Call to CallFunc
- Adjusts the behavior of RegisterFunc so that names are
augmented to produce collision-resistant handles of the
form "file:line:name"
- Makes callFunc and Func.call private for now, until
there's a clear need for them to be exported
- Eliminates gosh.Run
- Renames Cmd.Shutdown (which we decided to keep) to
Cmd.Terminate
- Changes Cmd.Signal and Cmd.Terminate to fail if Wait
has been called
- Drops Cmd.Kill; if it proves necessary, we'll add a
gosh.Kill implementation of os.Signal that tells
Cmd.Signal and Cmd.Terminate to issue Process.Kill

Also, updates v23test:
- Same changes as in gosh (Fn->FuncCmd, no more Main)
- Replaces v23test.Run with TestMain (simple case) and
InitTestMain (advanced case)
- Eliminates the hack with credentials env vars

MultiPart: 3/8

Change-Id: I43458981706f0bd5c694c898a1213f075d208282
diff --git a/gosh/.api b/gosh/.api
index 687f358..d8cead2 100644
--- a/gosh/.api
+++ b/gosh/.api
@@ -1,11 +1,9 @@
-pkg gosh, func Call(string, ...interface{}) error
 pkg gosh, func InitChildMain()
 pkg gosh, func InitMain()
 pkg gosh, func NewBufferedPipe() io.ReadWriteCloser
 pkg gosh, func NewShell(Opts) *Shell
 pkg gosh, func NopWriteCloser(io.Writer) io.WriteCloser
-pkg gosh, func Register(string, interface{}) *Fn
-pkg gosh, func Run(func() int) int
+pkg gosh, func RegisterFunc(string, interface{}) *Func
 pkg gosh, func SendReady()
 pkg gosh, func SendVars(map[string]string)
 pkg gosh, method (*Cmd) AddStderrWriter(io.WriteCloser)
@@ -14,10 +12,8 @@
 pkg gosh, method (*Cmd) AwaitVars(...string) map[string]string
 pkg gosh, method (*Cmd) Clone() *Cmd
 pkg gosh, method (*Cmd) CombinedOutput() string
-pkg gosh, method (*Cmd) Kill()
 pkg gosh, method (*Cmd) Pid() int
 pkg gosh, method (*Cmd) Run()
-pkg gosh, method (*Cmd) Shutdown(os.Signal)
 pkg gosh, method (*Cmd) Signal(os.Signal)
 pkg gosh, method (*Cmd) Start()
 pkg gosh, method (*Cmd) StderrPipe() io.Reader
@@ -25,15 +21,14 @@
 pkg gosh, method (*Cmd) Stdout() string
 pkg gosh, method (*Cmd) StdoutPipe() io.Reader
 pkg gosh, method (*Cmd) StdoutStderr() (string, string)
+pkg gosh, method (*Cmd) Terminate(os.Signal)
 pkg gosh, method (*Cmd) Wait()
-pkg gosh, method (*Fn) Call(...interface{}) error
 pkg gosh, method (*Shell) AddToCleanup(func())
 pkg gosh, method (*Shell) BuildGoPkg(string, ...string) string
 pkg gosh, method (*Shell) Cleanup()
 pkg gosh, method (*Shell) Cmd(string, ...string) *Cmd
-pkg gosh, method (*Shell) Fn(*Fn, ...interface{}) *Cmd
+pkg gosh, method (*Shell) FuncCmd(*Func, ...interface{}) *Cmd
 pkg gosh, method (*Shell) HandleError(error)
-pkg gosh, method (*Shell) Main(*Fn, ...string) *Cmd
 pkg gosh, method (*Shell) MakeTempDir() string
 pkg gosh, method (*Shell) MakeTempFile() *os.File
 pkg gosh, method (*Shell) Ok()
@@ -52,7 +47,7 @@
 pkg gosh, type Cmd struct, PropagateOutput bool
 pkg gosh, type Cmd struct, Stdin string
 pkg gosh, type Cmd struct, Vars map[string]string
-pkg gosh, type Fn struct
+pkg gosh, type Func struct
 pkg gosh, type Opts struct
 pkg gosh, type Opts struct, BinDir string
 pkg gosh, type Opts struct, ChildOutputDir string
diff --git a/gosh/cmd.go b/gosh/cmd.go
index 6ed9682..9fa6d1f 100644
--- a/gosh/cmd.go
+++ b/gosh/cmd.go
@@ -43,11 +43,11 @@
 	Args []string
 	// IgnoreParentExit, if true, makes it so the child process does not exit when
 	// its parent exits. Only takes effect if the child process was spawned via
-	// Shell.Fn or Shell.Main, or explicitly calls InitChildMain.
+	// Shell.FuncCmd or explicitly calls InitChildMain.
 	IgnoreParentExit bool
 	// ExitAfter, if non-zero, specifies that the child process should exit after
 	// the given duration has elapsed. Only takes effect if the child process was
-	// spawned via Shell.Fn or Shell.Main, or explicitly calls InitChildMain.
+	// spawned via Shell.FuncCmd or explicitly calls InitChildMain.
 	ExitAfter time.Duration
 	// PropagateOutput is inherited from Shell.Opts.PropagateChildOutput.
 	PropagateOutput bool
@@ -174,22 +174,18 @@
 	c.handleError(c.wait())
 }
 
-// Kill causes the process to exit immediately.
-func (c *Cmd) Kill() {
-	c.sh.Ok()
-	c.handleError(c.kill())
-}
-
 // Signal sends a signal to the process.
 func (c *Cmd) Signal(sig os.Signal) {
 	c.sh.Ok()
 	c.handleError(c.signal(sig))
 }
 
-// Shutdown sends a signal to the process, then waits for it to exit.
-func (c *Cmd) Shutdown(sig os.Signal) {
+// Terminate sends a signal to the process, then waits for it to exit. Terminate
+// is different from Signal followed by Wait: Terminate succeeds as long as the
+// process exits, whereas Wait fails if the exit code isn't 0.
+func (c *Cmd) Terminate(sig os.Signal) {
 	c.sh.Ok()
-	c.handleError(c.shutdown(sig))
+	c.handleError(c.terminate(sig))
 }
 
 // Run calls Start followed by Wait.
@@ -408,10 +404,9 @@
 }
 
 func (c *Cmd) clone() (*Cmd, error) {
-	vars := copyMap(c.Vars)
 	args := make([]string, len(c.Args))
 	copy(args, c.Args)
-	res, err := newCmdInternal(c.sh, vars, c.Path, args[1:])
+	res, err := newCmdInternal(c.sh, copyMap(c.Vars), c.Path, args[1:])
 	if err != nil {
 		return nil, err
 	}
@@ -617,22 +612,16 @@
 // https://golang.org/src/os/exec_windows.go
 const errFinished = "os: process already finished"
 
-func (c *Cmd) kill() error {
-	if !c.started {
-		return errDidNotCallStart
-	}
-	if !c.isRunning() {
-		return nil
-	}
-	if err := c.c.Process.Kill(); err != nil && err.Error() != errFinished {
-		return err
-	}
-	return nil
-}
-
+// NOTE(sadovsky): Technically speaking, Process.Signal(os.Kill) is different
+// from Process.Kill. Currently, gosh.Cmd does not provide a way to trigger
+// Process.Kill. If it proves necessary, we'll add a "gosh.Kill" implementation
+// of the os.Signal interface, and have the signal and terminate methods map
+// that to Process.Kill.
 func (c *Cmd) signal(sig os.Signal) error {
 	if !c.started {
 		return errDidNotCallStart
+	} else if c.calledWait {
+		return errAlreadyCalledWait
 	}
 	if !c.isRunning() {
 		return nil
@@ -643,11 +632,12 @@
 	return nil
 }
 
-func (c *Cmd) shutdown(sig os.Signal) error {
+func (c *Cmd) terminate(sig os.Signal) error {
 	if err := c.signal(sig); err != nil {
 		return err
 	}
 	if err := c.wait(); err != nil {
+		// Succeed as long as the process exited, regardless of the exit code.
 		if _, ok := err.(*exec.ExitError); !ok {
 			return err
 		}
diff --git a/gosh/internal/gosh_example/main.go b/gosh/internal/gosh_example/main.go
index d79d680..d3ecab2 100644
--- a/gosh/internal/gosh_example/main.go
+++ b/gosh/internal/gosh_example/main.go
@@ -11,7 +11,7 @@
 	"v.io/x/lib/gosh/internal/gosh_example_lib"
 )
 
-func ExampleCmds() {
+func ExampleCmd() {
 	sh := gosh.NewShell(gosh.Opts{})
 	defer sh.Cleanup()
 
@@ -30,37 +30,28 @@
 }
 
 var (
-	getFn   = gosh.Register("getFn", lib.Get)
-	serveFn = gosh.Register("serveFn", lib.Serve)
+	getFunc   = gosh.RegisterFunc("getFunc", lib.Get)
+	serveFunc = gosh.RegisterFunc("serveFunc", lib.Serve)
 )
 
-func ExampleFns() {
+func ExampleFuncCmd() {
 	sh := gosh.NewShell(gosh.Opts{})
 	defer sh.Cleanup()
 
 	// Start server.
-	c := sh.Fn(serveFn)
+	c := sh.FuncCmd(serveFunc)
 	c.Start()
 	c.AwaitReady()
 	addr := c.AwaitVars("Addr")["Addr"]
 	fmt.Println(addr)
 
 	// Run client.
-	c = sh.Fn(getFn, addr)
-	fmt.Print(c.Stdout())
-}
-
-func ExampleShellMain() {
-	sh := gosh.NewShell(gosh.Opts{})
-	defer sh.Cleanup()
-
-	c := sh.Main(lib.HelloWorldMain)
+	c = sh.FuncCmd(getFunc, addr)
 	fmt.Print(c.Stdout())
 }
 
 func main() {
 	gosh.InitMain()
-	ExampleCmds()
-	ExampleFns()
-	ExampleShellMain()
+	ExampleCmd()
+	ExampleFuncCmd()
 }
diff --git a/gosh/internal/gosh_example_lib/lib.go b/gosh/internal/gosh_example_lib/lib.go
index c2158d4..be51c33 100644
--- a/gosh/internal/gosh_example_lib/lib.go
+++ b/gosh/internal/gosh_example_lib/lib.go
@@ -15,13 +15,6 @@
 	"v.io/x/lib/gosh"
 )
 
-const helloWorld = "Hello, world!"
-
-// HelloWorldMain is used to demonstrate usage of Shell.Main.
-var HelloWorldMain = gosh.Register("HelloWorldMain", func() {
-	fmt.Println(helloWorld)
-})
-
 func Get(addr string) {
 	resp, err := http.Get("http://" + addr)
 	if err != nil {
@@ -49,7 +42,7 @@
 
 func Serve() {
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintln(w, helloWorld)
+		fmt.Fprintln(w, "Hello, world!")
 	})
 	// Note: With http.ListenAndServe() there's no easy way to tell which port
 	// number we were assigned, so instead we use net.Listen() followed by
diff --git a/gosh/registry.go b/gosh/registry.go
index 139a4f1..64453a8 100644
--- a/gosh/registry.go
+++ b/gosh/registry.go
@@ -13,29 +13,34 @@
 	"errors"
 	"fmt"
 	"reflect"
+	"runtime"
+	"sync"
 )
 
-// Fn is a registered, callable function.
-type Fn struct {
-	name  string
-	value reflect.Value
+// Func is a registered, callable function.
+type Func struct {
+	handle string
+	value  reflect.Value
 }
 
 var (
-	fns       = map[string]*Fn{}
 	errorType = reflect.TypeOf((*error)(nil)).Elem()
+	funcsMu   = sync.RWMutex{} // protects funcs
+	funcs     = map[string]*Func{}
 )
 
-// Register registers the given function with the given name. 'name' must be
-// unique across the dependency graph; 'fni' must be a function that accepts
-// gob-encodable arguments and returns an error or nothing.
-func Register(name string, fni interface{}) *Fn {
-	// TODO(sadovsky): Include len(fns) in registered name if it turns out that
-	// initialization order is deterministic.
-	if _, ok := fns[name]; ok {
-		panic(fmt.Errorf("gosh: %q is already registered", name))
+// RegisterFunc registers the given function with the given name. 'fi' must be a
+// function that accepts gob-encodable arguments and returns an error or
+// nothing.
+func RegisterFunc(name string, fi interface{}) *Func {
+	funcsMu.Lock()
+	defer funcsMu.Unlock()
+	_, file, line, _ := runtime.Caller(1)
+	handle := fmt.Sprintf("%s:%d:%s", file, line, name)
+	if _, ok := funcs[handle]; ok {
+		panic(fmt.Errorf("gosh: %q is already registered", handle))
 	}
-	v := reflect.ValueOf(fni)
+	v := reflect.ValueOf(fi)
 	t := v.Type()
 	if t.Kind() != reflect.Func {
 		panic(fmt.Errorf("gosh: %q is not a function: %v", name, t.Kind()))
@@ -43,7 +48,7 @@
 	if t.NumOut() > 1 || t.NumOut() == 1 && t.Out(0) != errorType {
 		panic(fmt.Errorf("gosh: %q must return an error or nothing: %v", name, t))
 	}
-	// Register the function's args with gob. Needed because Shell.Fn takes
+	// 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
@@ -53,23 +58,34 @@
 		}
 		gob.Register(reflect.Zero(t.In(i)).Interface())
 	}
-	fn := &Fn{name: name, value: v}
-	fns[name] = fn
-	return fn
+	f := &Func{handle: handle, value: v}
+	funcs[handle] = f
+	return f
 }
 
-// Call calls the named function, which must have been registered.
-func Call(name string, args ...interface{}) error {
-	fn, err := getFn(name)
+// getFunc returns the referenced function.
+func getFunc(handle string) (*Func, error) {
+	funcsMu.RLock()
+	f, ok := funcs[handle]
+	funcsMu.RUnlock()
+	if !ok {
+		return nil, fmt.Errorf("gosh: unknown function %q", handle)
+	}
+	return f, nil
+}
+
+// callFunc calls the referenced function, which must have been registered.
+func callFunc(handle string, args ...interface{}) error {
+	f, err := getFunc(handle)
 	if err != nil {
 		return err
 	}
-	return fn.Call(args...)
+	return f.call(args...)
 }
 
-// Call calls the function fn with the input arguments args.
-func (fn *Fn) Call(args ...interface{}) error {
-	t := fn.value.Type()
+// call calls this Func with the given input arguments.
+func (f *Func) call(args ...interface{}) error {
+	t := f.value.Type()
 	in := []reflect.Value{}
 	for i, arg := range args {
 		var av reflect.Value
@@ -82,22 +98,13 @@
 		}
 		in = append(in, av)
 	}
-	out := fn.value.Call(in)
+	out := f.value.Call(in)
 	if t.NumOut() == 1 && !out[0].IsNil() {
 		return out[0].Interface().(error)
 	}
 	return nil
 }
 
-// getFn returns the named function.
-func getFn(name string) (*Fn, error) {
-	fn, ok := fns[name]
-	if !ok {
-		return nil, fmt.Errorf("gosh: unknown function %q", name)
-	}
-	return fn, nil
-}
-
 // argType returns the type of the nth argument to a function of type t.
 func argType(t reflect.Type, n int) reflect.Type {
 	if !t.IsVariadic() || n < t.NumIn()-1 {
@@ -106,14 +113,14 @@
 	return t.In(t.NumIn() - 1).Elem()
 }
 
-// checkCall checks that the named function exists and can be called with the
-// given arguments. Modeled after the implementation of reflect.Value.call.
-func checkCall(name string, args ...interface{}) error {
-	fn, err := getFn(name)
+// checkCall checks that the referenced function exists and can be called with
+// the given arguments. Modeled after the implementation of reflect.Value.call.
+func checkCall(handle string, args ...interface{}) error {
+	f, err := getFunc(handle)
 	if err != nil {
 		return err
 	}
-	t := fn.value.Type()
+	t := f.value.Type()
 	n := t.NumIn()
 	if t.IsVariadic() {
 		n--
@@ -139,16 +146,16 @@
 // invocation
 
 type invocation struct {
-	Name string
-	Args []interface{}
+	Handle string
+	Args   []interface{}
 }
 
-// encInvocation encodes an invocation.
-func encInvocation(name string, args ...interface{}) (string, error) {
-	if err := checkCall(name, args...); err != nil {
+// encodeInvocation encodes an invocation.
+func encodeInvocation(handle string, args ...interface{}) (string, error) {
+	if err := checkCall(handle, args...); err != nil {
 		return "", err
 	}
-	inv := invocation{Name: name, Args: args}
+	inv := invocation{Handle: handle, Args: args}
 	buf := &bytes.Buffer{}
 	if err := gob.NewEncoder(buf).Encode(inv); err != nil {
 		return "", fmt.Errorf("gosh: failed to encode invocation: %v", err)
@@ -158,8 +165,8 @@
 	return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
 }
 
-// decInvocation decodes an invocation.
-func decInvocation(s string) (name string, args []interface{}, err error) {
+// decodeInvocation decodes an invocation.
+func decodeInvocation(s string) (handle string, args []interface{}, err error) {
 	var inv invocation
 	b, err := base64.StdEncoding.DecodeString(s)
 	if err == nil {
@@ -168,5 +175,5 @@
 	if err != nil {
 		return "", nil, fmt.Errorf("gosh: failed to decode invocation: %v", err)
 	}
-	return inv.Name, inv.Args, nil
+	return inv.Handle, inv.Args, nil
 }
diff --git a/gosh/shell.go b/gosh/shell.go
index 37d3174..59a3171 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -43,14 +43,6 @@
 	errDidNotCallNewShell   = errors.New("gosh: did not call gosh.NewShell")
 )
 
-// TODO(sadovsky):
-// - Eliminate Shell.Main, since it's easy enough to use Shell.Fn and set the
-//   returned Cmd's Args.
-// - Rename Shell.Fn to Shell.FuncCmd, Fn to Func, Register to RegisterFunc, and
-//   Call to CallFunc.
-// - Eliminate gosh.Run.
-// - Revisit whether to have Cmd.Shutdown.
-
 // 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
@@ -70,7 +62,7 @@
 	cmds           []*Cmd
 	tempFiles      []*os.File
 	tempDirs       []string
-	cleanupFns     []func()
+	cleanupFuncs   []func()
 }
 
 // Opts configures Shell.
@@ -109,7 +101,8 @@
 	}
 }
 
-// Cmd returns a Cmd for an invocation of the named program.
+// Cmd returns a Cmd for an invocation of the named program. The given arguments
+// are passed to the child as command-line arguments.
 func (sh *Shell) Cmd(name string, args ...string) *Cmd {
 	sh.Ok()
 	res, err := sh.cmd(nil, name, args...)
@@ -117,22 +110,13 @@
 	return res
 }
 
-// Fn returns a Cmd for an invocation of the given registered Fn.
-func (sh *Shell) Fn(fn *Fn, args ...interface{}) *Cmd {
+// FuncCmd returns a Cmd for an invocation of the given registered Func. The
+// given arguments are gob-encoded in the parent process, then gob-decoded in
+// the child and passed to the Func as parameters. To specify command-line
+// arguments for the child invocation, append to the returned Cmd's Args.
+func (sh *Shell) FuncCmd(f *Func, args ...interface{}) *Cmd {
 	sh.Ok()
-	res, err := sh.fn(fn, args...)
-	sh.HandleError(err)
-	return res
-}
-
-// Main returns a Cmd for an invocation of the given registered main() function.
-// Intended usage: Have your program's main() call RealMain, then write a parent
-// program that uses Shell.Main to run RealMain in a child process. With this
-// approach, RealMain can be compiled into the parent program's binary. Caveat:
-// potential flag collisions.
-func (sh *Shell) Main(fn *Fn, args ...string) *Cmd {
-	sh.Ok()
-	res, err := sh.main(fn, args...)
+	res, err := sh.funcCmd(f, args...)
 	sh.HandleError(err)
 	return res
 }
@@ -192,15 +176,16 @@
 }
 
 // AddToCleanup registers the given function to be called by Shell.Cleanup().
-func (sh *Shell) AddToCleanup(fn func()) {
+func (sh *Shell) AddToCleanup(f func()) {
 	sh.Ok()
-	sh.HandleError(sh.addToCleanup(fn))
+	sh.HandleError(sh.addToCleanup(f))
 }
 
 // Cleanup cleans up all resources (child processes, temporary files and
 // directories) associated with this Shell. It is safe (and recommended) to call
 // Cleanup after a Shell error. It is also safe to call Cleanup multiple times;
-// calls after the first return immediately with no effect.
+// calls after the first return immediately with no effect. Cleanup never calls
+// HandleError.
 func (sh *Shell) Cleanup() {
 	if !sh.calledNewShell {
 		panic(errDidNotCallNewShell)
@@ -234,17 +219,18 @@
 
 // onTerminationSignal starts a goroutine that listens for various termination
 // signals and calls the given function when such a signal is received.
-func onTerminationSignal(fn func(os.Signal)) {
+func onTerminationSignal(f func(os.Signal)) {
 	ch := make(chan os.Signal, 1)
 	signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
 	go func() {
-		fn(<-ch)
+		f(<-ch)
 	}()
 }
 
 // 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...))
@@ -256,15 +242,20 @@
 		}
 	}
 	if opts.ChildOutputDir == "" {
-		opts.ChildOutputDir = os.Getenv(envChildOutputDir)
+		opts.ChildOutputDir = osVars[envChildOutputDir]
+	}
+	// Filter out any gosh env vars coming from outside.
+	shVars := copyMap(osVars)
+	for _, key := range []string{envBinDir, envChildOutputDir, envExitAfter, envInvocation, envWatchParent} {
+		delete(shVars, key)
 	}
 	sh := &Shell{
 		Opts:           opts,
-		Vars:           map[string]string{},
+		Vars:           shVars,
 		calledNewShell: true,
 	}
 	if sh.Opts.BinDir == "" {
-		sh.Opts.BinDir = os.Getenv(envBinDir)
+		sh.Opts.BinDir = osVars[envBinDir]
 		if sh.Opts.BinDir == "" {
 			var err error
 			if sh.Opts.BinDir, err = sh.makeTempDir(); err != nil {
@@ -298,9 +289,7 @@
 	if vars == nil {
 		vars = make(map[string]string)
 	}
-	// TODO(sadovsky): Copy os.Environ() into sh.Vars in NewShell, and clear
-	// envWatchParent and envExitAfter if they're set.
-	c, err := newCmd(sh, mergeMaps(sliceToMap(os.Environ()), sh.Vars, vars), name, append(args, sh.Args...)...)
+	c, err := newCmd(sh, mergeMaps(sh.Vars, vars), name, append(args, sh.Args...)...)
 	if err != nil {
 		return nil, err
 	}
@@ -318,39 +307,20 @@
 	}
 }
 
-func (sh *Shell) fn(fn *Fn, args ...interface{}) (*Cmd, error) {
+func (sh *Shell) funcCmd(f *Func, args ...interface{}) (*Cmd, error) {
 	// Safeguard against the developer forgetting to call InitMain, which could
 	// lead to infinite recursion.
 	if !calledInitMain {
 		return nil, errDidNotCallInitMain
 	}
-	b, err := encInvocation(fn.name, args...)
+	buf, err := encodeInvocation(f.handle, args...)
 	if err != nil {
 		return nil, err
 	}
-	vars := map[string]string{envInvocation: string(b)}
+	vars := map[string]string{envInvocation: string(buf)}
 	return sh.cmd(vars, executablePath)
 }
 
-func (sh *Shell) main(fn *Fn, args ...string) (*Cmd, error) {
-	// Safeguard against the developer forgetting to call InitMain, which could
-	// lead to infinite recursion.
-	if !calledInitMain {
-		return nil, errDidNotCallInitMain
-	}
-	// Check that fn has the required signature.
-	t := fn.value.Type()
-	if t.NumIn() != 0 || t.NumOut() != 0 {
-		return nil, errors.New("gosh: main function must have no input or output parameters")
-	}
-	b, err := encInvocation(fn.name)
-	if err != nil {
-		return nil, err
-	}
-	vars := map[string]string{envInvocation: string(b)}
-	return sh.cmd(vars, executablePath, args...)
-}
-
 func (sh *Shell) wait() error {
 	// Note: It is illegal to call newCmd() concurrently with Shell.wait(), so we
 	// need not hold cleanupMu when accessing sh.cmds below.
@@ -463,24 +433,24 @@
 	return nil
 }
 
-func (sh *Shell) addToCleanup(fn func()) error {
+func (sh *Shell) addToCleanup(f func()) error {
 	sh.cleanupMu.Lock()
 	defer sh.cleanupMu.Unlock()
 	if sh.calledCleanup {
 		return errAlreadyCalledCleanup
 	}
-	sh.cleanupFns = append(sh.cleanupFns, fn)
+	sh.cleanupFuncs = append(sh.cleanupFuncs, f)
 	return nil
 }
 
-// forEachRunningCmd applies fn to each running child process.
-func (sh *Shell) forEachRunningCmd(fn func(*Cmd)) bool {
+// forEachRunningCmd applies f to each running child process.
+func (sh *Shell) forEachRunningCmd(f func(*Cmd)) bool {
 	anyRunning := false
 	for _, c := range sh.cmds {
 		if c.isRunning() {
 			anyRunning = true
-			if fn != nil {
-				fn(c)
+			if f != nil {
+				f(c)
 			}
 		}
 	}
@@ -489,10 +459,10 @@
 
 // Note: It is safe to run Shell.terminateRunningCmds concurrently with the
 // waiter goroutine and with Cmd.wait. In particular, Shell.terminateRunningCmds
-// only calls c.{isRunning,Pid,Signal,Kill}, all of which are thread-safe with
-// the waiter goroutine and with Cmd.wait.
+// only calls c.{isRunning,Pid,signal}, all of which are thread-safe with the
+// waiter goroutine and with Cmd.wait.
 func (sh *Shell) terminateRunningCmds() {
-	// Try Cmd.signal first; if that doesn't work, use Cmd.kill.
+	// 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)
@@ -505,13 +475,13 @@
 			sh.logf("%s (PID %d) did not die\n", c.Path, c.Pid())
 		})
 	}
-	// If any child is still running, wait for another second, then call Cmd.kill
-	// for all running children.
+	// If any child is still running, wait for another second, then send os.Kill
+	// to all running children.
 	if anyRunning {
 		time.Sleep(time.Second)
 		sh.forEachRunningCmd(func(c *Cmd) {
-			if err := c.kill(); err != nil {
-				sh.logf("%d.Kill() failed: %v\n", c.Pid(), err)
+			if err := c.signal(os.Kill); err != nil {
+				sh.logf("%d.Signal(os.Kill) failed: %v\n", c.Pid(), err)
 			}
 		})
 		sh.logf("Killed all remaining child processes\n")
@@ -546,8 +516,8 @@
 		}
 	}
 	// Call any registered cleanup functions in LIFO order.
-	for i := len(sh.cleanupFns) - 1; i >= 0; i-- {
-		sh.cleanupFns[i]()
+	for i := len(sh.cleanupFuncs) - 1; i >= 0; i-- {
+		sh.cleanupFuncs[i]()
 	}
 }
 
@@ -558,7 +528,7 @@
 
 // InitMain must be called early on in main(), before flags are parsed. In the
 // parent process, it returns immediately with no effect. In a child process for
-// a Shell.Fn or Shell.Main command, it runs the specified function, then exits.
+// a Shell.FuncCmd command, it runs the specified function, then exits.
 func InitMain() {
 	calledInitMain = true
 	s := os.Getenv(envInvocation)
@@ -567,23 +537,16 @@
 	}
 	os.Unsetenv(envInvocation)
 	InitChildMain()
-	name, args, err := decInvocation(s)
+	name, args, err := decodeInvocation(s)
 	if err != nil {
 		log.Fatal(err)
 	}
-	if err := Call(name, args...); err != nil {
+	if err := callFunc(name, args...); err != nil {
 		log.Fatal(err)
 	}
 	os.Exit(0)
 }
 
-// Run calls InitMain, then returns run(). Exported so that TestMain functions
-// can simply call os.Exit(gosh.Run(m.Run)).
-func Run(run func() int) int {
-	InitMain()
-	return run()
-}
-
 // NopWriteCloser returns a WriteCloser with a no-op Close method wrapping the
 // provided Writer.
 func NopWriteCloser(w io.Writer) io.WriteCloser {
diff --git a/gosh/shell_test.go b/gosh/shell_test.go
index 73e5da0..f8e1840 100644
--- a/gosh/shell_test.go
+++ b/gosh/shell_test.go
@@ -6,10 +6,10 @@
 
 // TODO(sadovsky): Add more tests:
 // - effects of Shell.Cleanup
-// - Cmd.Clone
 // - Shell.{Vars,Args,Rename,MakeTempFile,MakeTempDir}
-// - Opts (including defaulting behavior)
-// - {,Maybe}WatchParent
+// - Shell.Opts.{PropagateChildOutput,ChildOutputDir,BinDir}
+// - Cmd.Clone
+// - Cmd.Opts.{IgnoreParentExit,ExitAfter,PropagateOutput}
 
 import (
 	"bufio"
@@ -19,6 +19,7 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"os/signal"
 	"path/filepath"
 	"reflect"
 	"runtime/debug"
@@ -78,35 +79,52 @@
 	}
 }
 
+func setsErr(t *testing.T, sh *gosh.Shell, f func()) {
+	calledFatalf := false
+	sh.Opts.Fatalf = func(string, ...interface{}) { calledFatalf = true }
+	f()
+	nok(t, sh.Err)
+	eq(t, calledFatalf, true)
+	sh.Err = nil
+	sh.Opts.Fatalf = makeFatalf(t)
+}
+
 ////////////////////////////////////////
 // Simple functions
 
 // Simplified versions of various Unix commands.
 var (
-	catFn = gosh.Register("catFn", func() {
+	catFunc = gosh.RegisterFunc("catFunc", func() {
 		io.Copy(os.Stdout, os.Stdin)
 	})
-	echoFn = gosh.Register("echoFn", func() {
+	echoFunc = gosh.RegisterFunc("echoFunc", func() {
 		fmt.Println(os.Args[1])
 	})
-	readFn = gosh.Register("readFn", func() {
+	readFunc = gosh.RegisterFunc("readFunc", func() {
 		bufio.NewReader(os.Stdin).ReadString('\n')
 	})
 )
 
 // Functions with parameters.
 var (
-	exitFn = gosh.Register("exitFn", func(code int) {
+	exitFunc = gosh.RegisterFunc("exitFunc", func(code int) {
 		os.Exit(code)
 	})
-	sleepFn = gosh.Register("sleepFn", func(d time.Duration, code int) {
+	sleepFunc = gosh.RegisterFunc("sleepFunc", func(d time.Duration, code int) {
+		// For TestSignal and TestTerminate.
+		ch := make(chan os.Signal, 1)
+		signal.Notify(ch, os.Interrupt)
+		go func() {
+			<-ch
+			os.Exit(0)
+		}()
 		time.Sleep(d)
 		os.Exit(code)
 	})
-	printFn = gosh.Register("printFn", func(v ...interface{}) {
+	printFunc = gosh.RegisterFunc("printFunc", func(v ...interface{}) {
 		fmt.Print(v...)
 	})
-	printfFn = gosh.Register("printfFn", func(format string, v ...interface{}) {
+	printfFunc = gosh.RegisterFunc("printfFunc", func(format string, v ...interface{}) {
 		fmt.Printf(format, v...)
 	})
 )
@@ -151,9 +169,7 @@
 	ok(t, err)
 	eq(t, cwd, startDir)
 	// The next sh.Popd() will fail.
-	sh.Opts.Fatalf = nil
-	sh.Popd()
-	nok(t, sh.Err)
+	setsErr(t, sh, func() { sh.Popd() })
 }
 
 func evalSymlinks(t *testing.T, dir string) string {
@@ -182,7 +198,7 @@
 	eq(t, getwdEvalSymlinks(t), startDir)
 }
 
-func TestCmds(t *testing.T) {
+func TestCmd(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
@@ -201,44 +217,36 @@
 }
 
 var (
-	getFn   = gosh.Register("getFn", lib.Get)
-	serveFn = gosh.Register("serveFn", lib.Serve)
+	getFunc   = gosh.RegisterFunc("getFunc", lib.Get)
+	serveFunc = gosh.RegisterFunc("serveFunc", lib.Serve)
 )
 
-func TestFns(t *testing.T) {
+func TestFuncCmd(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
 	// Start server.
-	c := sh.Fn(serveFn)
+	c := sh.FuncCmd(serveFunc)
 	c.Start()
 	c.AwaitReady()
 	addr := c.AwaitVars("Addr")["Addr"]
 	neq(t, addr, "")
 
 	// Run client.
-	c = sh.Fn(getFn, addr)
-	eq(t, c.Stdout(), "Hello, world!\n")
-}
-
-func TestShellMain(t *testing.T) {
-	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
-	defer sh.Cleanup()
-
-	c := sh.Main(lib.HelloWorldMain)
+	c = sh.FuncCmd(getFunc, addr)
 	eq(t, c.Stdout(), "Hello, world!\n")
 }
 
 // Functions designed for TestRegistry.
 var (
-	printIntsFn = gosh.Register("printIntsFn", func(v ...int) {
+	printIntsFunc = gosh.RegisterFunc("printIntsFunc", func(v ...int) {
 		var vi []interface{}
 		for _, x := range v {
 			vi = append(vi, x)
 		}
 		fmt.Print(vi...)
 	})
-	printfIntsFn = gosh.Register("printfIntsFn", func(format string, v ...int) {
+	printfIntsFunc = gosh.RegisterFunc("printfIntsFunc", func(format string, v ...int) {
 		var vi []interface{}
 		for _, x := range v {
 			vi = append(vi, x)
@@ -252,21 +260,13 @@
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	c := sh.Fn(exitFn, 0)
+	c := sh.FuncCmd(exitFunc, 0)
 	c.Start()
-	sh.Opts.Fatalf = nil
-	c.AwaitReady()
-	nok(t, sh.Err)
-	sh.Err = nil
-	sh.Opts.Fatalf = makeFatalf(t)
+	setsErr(t, sh, func() { c.AwaitReady() })
 
-	c = sh.Fn(exitFn, 0)
+	c = sh.FuncCmd(exitFunc, 0)
 	c.Start()
-	sh.Opts.Fatalf = nil
-	c.AwaitVars("foo")
-	nok(t, sh.Err)
-	sh.Err = nil
-	sh.Opts.Fatalf = makeFatalf(t)
+	setsErr(t, sh, func() { c.AwaitVars("foo") })
 }
 
 // Tests function signature-checking and execution.
@@ -276,105 +276,81 @@
 
 	// Variadic functions. Non-variadic functions are sufficiently covered in
 	// other tests.
-	eq(t, sh.Fn(printFn).Stdout(), "")
-	eq(t, sh.Fn(printFn, 0).Stdout(), "0")
-	eq(t, sh.Fn(printFn, 0, "foo").Stdout(), "0foo")
-	eq(t, sh.Fn(printfFn, "").Stdout(), "")
-	eq(t, sh.Fn(printfFn, "%v", 0).Stdout(), "0")
-	eq(t, sh.Fn(printfFn, "%v%v", 0, "foo").Stdout(), "0foo")
-	eq(t, sh.Fn(printIntsFn, 1, 2).Stdout(), "1 2")
-	eq(t, sh.Fn(printfIntsFn, "%v %v", 1, 2).Stdout(), "1 2")
-
-	// Error cases.
-	sh.Opts.Fatalf = nil
-	reset := func() {
-		nok(t, sh.Err)
-		sh.Err = nil
-	}
+	eq(t, sh.FuncCmd(printFunc).Stdout(), "")
+	eq(t, sh.FuncCmd(printFunc, 0).Stdout(), "0")
+	eq(t, sh.FuncCmd(printFunc, 0, "foo").Stdout(), "0foo")
+	eq(t, sh.FuncCmd(printfFunc, "").Stdout(), "")
+	eq(t, sh.FuncCmd(printfFunc, "%v", 0).Stdout(), "0")
+	eq(t, sh.FuncCmd(printfFunc, "%v%v", 0, "foo").Stdout(), "0foo")
+	eq(t, sh.FuncCmd(printIntsFunc, 1, 2).Stdout(), "1 2")
+	eq(t, sh.FuncCmd(printfIntsFunc, "%v %v", 1, 2).Stdout(), "1 2")
 
 	// Too few arguments.
-	sh.Fn(exitFn)
-	reset()
-	sh.Fn(sleepFn, time.Second)
-	reset()
-	sh.Fn(printfFn)
-	reset()
+	setsErr(t, sh, func() { sh.FuncCmd(exitFunc) })
+	setsErr(t, sh, func() { sh.FuncCmd(sleepFunc, time.Second) })
+	setsErr(t, sh, func() { sh.FuncCmd(printfFunc) })
 
 	// Too many arguments.
-	sh.Fn(exitFn, 0, 0)
-	reset()
-	sh.Fn(sleepFn, time.Second, 0, 0)
-	reset()
+	setsErr(t, sh, func() { sh.FuncCmd(exitFunc, 0, 0) })
+	setsErr(t, sh, func() { sh.FuncCmd(sleepFunc, time.Second, 0, 0) })
 
 	// Wrong argument types.
-	sh.Fn(exitFn, "foo")
-	reset()
-	sh.Fn(sleepFn, 0, 0)
-	reset()
-	sh.Fn(printfFn, 0)
-	reset()
-	sh.Fn(printfFn, 0, 0)
-	reset()
+	setsErr(t, sh, func() { sh.FuncCmd(exitFunc, "foo") })
+	setsErr(t, sh, func() { sh.FuncCmd(sleepFunc, 0, 0) })
+	setsErr(t, sh, func() { sh.FuncCmd(printfFunc, 0) })
+	setsErr(t, sh, func() { sh.FuncCmd(printfFunc, 0, 0) })
 
 	// Wrong variadic argument types.
-	sh.Fn(printIntsFn, 0.5)
-	reset()
-	sh.Fn(printIntsFn, 0, 0.5)
-	reset()
-	sh.Fn(printfIntsFn, "%v", 0.5)
-	reset()
-	sh.Fn(printfIntsFn, "%v", 0, 0.5)
-	reset()
+	setsErr(t, sh, func() { sh.FuncCmd(printIntsFunc, 0.5) })
+	setsErr(t, sh, func() { sh.FuncCmd(printIntsFunc, 0, 0.5) })
+	setsErr(t, sh, func() { sh.FuncCmd(printfIntsFunc, "%v", 0.5) })
+	setsErr(t, sh, func() { sh.FuncCmd(printfIntsFunc, "%v", 0, 0.5) })
 
 	// Unsupported argument types.
 	var p *int
-	sh.Fn(printFn, p)
-	reset()
-	sh.Fn(printfFn, "%v", p)
-	reset()
+	setsErr(t, sh, func() { sh.FuncCmd(printFunc, p) })
+	setsErr(t, sh, func() { sh.FuncCmd(printfFunc, "%v", p) })
 }
 
 func TestStdin(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	c := sh.Main(catFn)
+	c := sh.FuncCmd(catFunc)
 	c.Stdin = "foo\n"
 	// We set c.Stdin and did not call c.StdinPipe(), so stdin should close and
 	// cat should exit immediately.
 	eq(t, c.Stdout(), "foo\n")
 
-	c = sh.Main(catFn)
+	c = sh.FuncCmd(catFunc)
 	c.StdinPipe().Write([]byte("foo\n"))
 	// The "cat" command only exits when stdin is closed, so we must explicitly
 	// close the stdin pipe. Note, it's safe to call c.StdinPipe multiple times.
 	c.StdinPipe().Close()
 	eq(t, c.Stdout(), "foo\n")
 
-	c = sh.Main(readFn)
+	c = sh.FuncCmd(readFunc)
 	c.StdinPipe().Write([]byte("foo\n"))
 	// The "read" command exits when it sees a newline, so Cmd.Wait (and thus
 	// Cmd.Run) should return immediately; it should not be necessary to close the
 	// stdin pipe.
 	c.Run()
 
-	c = sh.Main(catFn)
+	c = sh.FuncCmd(catFunc)
 	// No stdin, so cat should exit immediately.
 	eq(t, c.Stdout(), "")
 
 	// It's an error (detected at command start time) to both set c.Stdin and call
 	// c.StdinPipe. Note, this indirectly tests that Shell.Cleanup works even if
 	// some Cmd.Start failed.
-	c = sh.Main(catFn)
+	c = sh.FuncCmd(catFunc)
 	c.Stdin = "foo"
 	c.StdinPipe().Write([]byte("bar"))
 	c.StdinPipe().Close()
-	sh.Opts.Fatalf = nil
-	c.Start()
-	nok(t, sh.Err)
+	setsErr(t, sh, func() { c.Start() })
 }
 
-var writeFn = gosh.Register("writeFn", func(stdout, stderr bool) error {
+var writeFunc = gosh.RegisterFunc("writeFunc", func(stdout, stderr bool) error {
 	if stdout {
 		if _, err := os.Stdout.Write([]byte("A")); err != nil {
 			return err
@@ -403,7 +379,7 @@
 	defer sh.Cleanup()
 
 	// Write to stdout only.
-	c := sh.Fn(writeFn, true, false)
+	c := sh.FuncCmd(writeFunc, true, false)
 	stdoutPipe, stderrPipe := c.StdoutPipe(), c.StderrPipe()
 	stdout, stderr := c.StdoutStderr()
 	eq(t, stdout, "AA")
@@ -412,7 +388,7 @@
 	eq(t, toString(t, stderrPipe), "")
 
 	// Write to stderr only.
-	c = sh.Fn(writeFn, false, true)
+	c = sh.FuncCmd(writeFunc, false, true)
 	stdoutPipe, stderrPipe = c.StdoutPipe(), c.StderrPipe()
 	stdout, stderr = c.StdoutStderr()
 	eq(t, stdout, "")
@@ -421,7 +397,7 @@
 	eq(t, toString(t, stderrPipe), "BB")
 
 	// Write to both stdout and stderr.
-	c = sh.Fn(writeFn, true, true)
+	c = sh.FuncCmd(writeFunc, true, true)
 	stdoutPipe, stderrPipe = c.StdoutPipe(), c.StderrPipe()
 	stdout, stderr = c.StdoutStderr()
 	eq(t, stdout, "AA")
@@ -430,11 +406,11 @@
 	eq(t, toString(t, stderrPipe), "BB")
 }
 
-var writeMoreFn = gosh.Register("writeMoreFn", func() {
+var writeMoreFunc = gosh.RegisterFunc("writeMoreFunc", func() {
 	sh := gosh.NewShell(gosh.Opts{})
 	defer sh.Cleanup()
 
-	c := sh.Fn(writeFn, true, true)
+	c := sh.FuncCmd(writeFunc, true, true)
 	c.AddStdoutWriter(gosh.NopWriteCloser(os.Stdout))
 	c.AddStderrWriter(gosh.NopWriteCloser(os.Stderr))
 	c.Run()
@@ -448,7 +424,7 @@
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	stdout, stderr := sh.Fn(writeMoreFn).StdoutStderr()
+	stdout, stderr := sh.FuncCmd(writeMoreFunc).StdoutStderr()
 	eq(t, stdout, "AA stdout done")
 	eq(t, stderr, "BB stderr done")
 }
@@ -458,27 +434,18 @@
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	c := sh.Fn(writeMoreFn)
-	sh.Opts.Fatalf = nil
-	c.AddStdoutWriter(os.Stdout)
-	nok(t, sh.Err)
-	sh.Err = nil
-	c.AddStdoutWriter(os.Stderr)
-	nok(t, sh.Err)
-	sh.Err = nil
-	c.AddStderrWriter(os.Stdout)
-	nok(t, sh.Err)
-	sh.Err = nil
-	c.AddStderrWriter(os.Stderr)
-	nok(t, sh.Err)
-	sh.Err = nil
+	c := sh.FuncCmd(writeMoreFunc)
+	setsErr(t, sh, func() { c.AddStdoutWriter(os.Stdout) })
+	setsErr(t, sh, func() { c.AddStdoutWriter(os.Stderr) })
+	setsErr(t, sh, func() { c.AddStderrWriter(os.Stdout) })
+	setsErr(t, sh, func() { c.AddStderrWriter(os.Stderr) })
 }
 
 func TestCombinedOutput(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	c := sh.Fn(writeFn, true, true)
+	c := sh.FuncCmd(writeFunc, true, true)
 	buf := &bytes.Buffer{}
 	c.AddStdoutWriter(gosh.NopWriteCloser(buf))
 	c.AddStderrWriter(gosh.NopWriteCloser(buf))
@@ -496,7 +463,7 @@
 	defer sh.Cleanup()
 
 	dir := sh.MakeTempDir()
-	c := sh.Fn(writeFn, true, true)
+	c := sh.FuncCmd(writeFunc, true, true)
 	c.OutputDir = dir
 	c.Run()
 
@@ -531,7 +498,7 @@
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	c := sh.Fn(writeFn, true, true)
+	c := sh.FuncCmd(writeFunc, true, true)
 	buf := &bytes.Buffer{}
 	wc := &countingWriteCloser{Writer: buf}
 	c.AddStdoutWriter(wc)
@@ -551,15 +518,16 @@
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	echo := sh.Main(echoFn, "foo")
-	cat := sh.Main(catFn)
+	echo := sh.FuncCmd(echoFunc)
+	echo.Args = append(echo.Args, "foo")
+	cat := sh.FuncCmd(catFunc)
 	echo.AddStdoutWriter(cat.StdinPipe())
 	echo.Start()
 	eq(t, cat.Stdout(), "foo\n")
 
 	// This time, pipe both stdout and stderr to cat's stdin.
-	c := sh.Fn(writeFn, true, true)
-	cat = sh.Main(catFn)
+	c := sh.FuncCmd(writeFunc, true, true)
+	cat = sh.FuncCmd(catFunc)
 	c.AddStdoutWriter(cat.StdinPipe())
 	c.AddStderrWriter(cat.StdinPipe())
 	c.Start()
@@ -568,21 +536,57 @@
 	eq(t, len(cat.Stdout()), 4)
 }
 
-func TestShutdown(t *testing.T) {
+func TestSignal(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
 	for _, d := range []time.Duration{0, time.Second} {
 		for _, s := range []os.Signal{os.Interrupt, os.Kill} {
 			fmt.Println(d, s)
-			c := sh.Fn(sleepFn, d, 0)
+			c := sh.FuncCmd(sleepFunc, d, 0)
 			c.Start()
-			// Wait for a bit to allow the zero-sleep commands to exit, to test that
-			// Shutdown succeeds for an exited process.
+			// Wait for a bit to allow the zero-sleep commands to exit.
 			time.Sleep(100 * time.Millisecond)
-			c.Shutdown(s)
+			c.Signal(s)
+			// Wait should succeed as long as the exit code was 0, regardless of
+			// whether the signal arrived or the process had already exited.
+			if s == os.Interrupt {
+				// Note: We don't call Wait in the {d: 0, s: os.Kill} case because doing
+				// so makes the test flaky on slow systems.
+				c.Wait()
+			} else if d == time.Second {
+				setsErr(t, sh, func() { c.Wait() })
+			}
 		}
 	}
+
+	// Signal should fail if Wait has been called.
+	c := sh.FuncCmd(sleepFunc, time.Duration(0), 0)
+	c.Run()
+	setsErr(t, sh, func() { c.Signal(os.Interrupt) })
+}
+
+func TestTerminate(t *testing.T) {
+	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+	defer sh.Cleanup()
+
+	for _, d := range []time.Duration{0, time.Second} {
+		for _, s := range []os.Signal{os.Interrupt, os.Kill} {
+			fmt.Println(d, s)
+			c := sh.FuncCmd(sleepFunc, d, 0)
+			c.Start()
+			// Wait for a bit to allow the zero-sleep commands to exit.
+			time.Sleep(100 * time.Millisecond)
+			// Terminate should succeed regardless of the exit code, and regardless of
+			// whether the signal arrived or the process had already exited.
+			c.Terminate(s)
+		}
+	}
+
+	// Terminate should fail if Wait has been called.
+	c := sh.FuncCmd(sleepFunc, time.Duration(0), 0)
+	c.Run()
+	setsErr(t, sh, func() { c.Terminate(os.Interrupt) })
 }
 
 func TestShellWait(t *testing.T) {
@@ -592,14 +596,14 @@
 	d0 := time.Duration(0)
 	d200 := 200 * time.Millisecond
 
-	c0 := sh.Fn(sleepFn, d0, 0)   // not started
-	c1 := sh.Fn(sleepFn, d0, 0)   // failed to start
-	c2 := sh.Fn(sleepFn, d200, 0) // running and will succeed
-	c3 := sh.Fn(sleepFn, d200, 1) // running and will fail
-	c4 := sh.Fn(sleepFn, d0, 0)   // succeeded
-	c5 := sh.Fn(sleepFn, d0, 0)   // succeeded, called wait
-	c6 := sh.Fn(sleepFn, d0, 1)   // failed
-	c7 := sh.Fn(sleepFn, d0, 1)   // failed, called wait
+	c0 := sh.FuncCmd(sleepFunc, d0, 0)   // not started
+	c1 := sh.FuncCmd(sleepFunc, d0, 0)   // failed to start
+	c2 := sh.FuncCmd(sleepFunc, d200, 0) // running and will succeed
+	c3 := sh.FuncCmd(sleepFunc, d200, 1) // running and will fail
+	c4 := sh.FuncCmd(sleepFunc, d0, 0)   // succeeded
+	c5 := sh.FuncCmd(sleepFunc, d0, 0)   // succeeded, called wait
+	c6 := sh.FuncCmd(sleepFunc, d0, 1)   // failed
+	c7 := sh.FuncCmd(sleepFunc, d0, 1)   // failed, called wait
 
 	c3.ExitErrorIsOk = true
 	c6.ExitErrorIsOk = true
@@ -608,11 +612,7 @@
 	// Configure the "failed to start" command.
 	c1.StdinPipe()
 	c1.Stdin = "foo"
-	sh.Opts.Fatalf = nil
-	c1.Start()
-	nok(t, sh.Err)
-	sh.Err = nil
-	sh.Opts.Fatalf = makeFatalf(t)
+	setsErr(t, sh, func() { c1.Start() })
 
 	// Start commands, then wait for them to exit.
 	for _, c := range []*gosh.Cmd{c2, c3, c4, c5, c6, c7} {
@@ -627,8 +627,8 @@
 	// It should be possible to run existing unstarted commands, and to create and
 	// run new commands, after calling Shell.Wait.
 	c0.Run()
-	sh.Fn(sleepFn, d0, 0).Run()
-	sh.Fn(sleepFn, d0, 0).Start()
+	sh.FuncCmd(sleepFunc, d0, 0).Run()
+	sh.FuncCmd(sleepFunc, d0, 0).Start()
 
 	// Call Shell.Wait again.
 	sh.Wait()
@@ -639,24 +639,22 @@
 	defer sh.Cleanup()
 
 	// Exit code 0 is not an error.
-	c := sh.Fn(exitFn, 0)
+	c := sh.FuncCmd(exitFunc, 0)
 	c.Run()
 	ok(t, c.Err)
 	ok(t, sh.Err)
 
 	// Exit code 1 is an error.
-	c = sh.Fn(exitFn, 1)
+	c = sh.FuncCmd(exitFunc, 1)
 	c.ExitErrorIsOk = true
 	c.Run()
 	nok(t, c.Err)
 	ok(t, sh.Err)
 
 	// If ExitErrorIsOk is false, exit code 1 triggers sh.HandleError.
-	c = sh.Fn(exitFn, 1)
-	sh.Opts.Fatalf = nil
-	c.Run()
+	c = sh.FuncCmd(exitFunc, 1)
+	setsErr(t, sh, func() { c.Run() })
 	nok(t, c.Err)
-	nok(t, sh.Err)
 }
 
 // Tests that sh.Ok panics under various conditions.
@@ -727,5 +725,6 @@
 }
 
 func TestMain(m *testing.M) {
-	os.Exit(gosh.Run(m.Run))
+	gosh.InitMain()
+	os.Exit(m.Run())
 }
diff --git a/vlog/flags_test.go b/vlog/flags_test.go
index f241165..f22ed59 100644
--- a/vlog/flags_test.go
+++ b/vlog/flags_test.go
@@ -15,7 +15,7 @@
 	"v.io/x/lib/vlog"
 )
 
-var child = gosh.Register("child", func() error {
+var child = gosh.RegisterFunc("child", func() error {
 	tmp := filepath.Join(os.TempDir(), "foo")
 	flag.Set("log_dir", tmp)
 	flag.Set("vmodule", "foo=2")
@@ -44,9 +44,10 @@
 func TestFlags(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
 	defer sh.Cleanup()
-	sh.Fn(child).Run()
+	sh.FuncCmd(child).Run()
 }
 
 func TestMain(m *testing.M) {
-	os.Exit(gosh.Run(m.Run))
+	gosh.InitMain()
+	os.Exit(m.Run())
 }