lib: Fix various cmdline documentation issues.

o Ensure the v.io/jiri tool only has docs for built-in commands.
o Add a v.io/x/devtools/jiridoc target for full jiri docs.
o Fix cmdline package to elide CMDLINE_* envvars before running
user-specified runners.
o Update cmdline gendoc.go to allow setting envvars, install
extra packages, and explicitly specify the output file.
o Add helper functions to envvar package to copy maps and slices.

MultiPart: 3/3

Change-Id: I6e50ce02b2c1e34ade6fd779c2e4336020de3fbb
diff --git a/cmdline/.api b/cmdline/.api
index 583cc7e..3f3d2df 100644
--- a/cmdline/.api
+++ b/cmdline/.api
@@ -23,7 +23,7 @@
 pkg cmdline, type Env struct, Stderr io.Writer
 pkg cmdline, type Env struct, Stdin io.Reader
 pkg cmdline, type Env struct, Stdout io.Writer
-pkg cmdline, type Env struct, Usage func(io.Writer)
+pkg cmdline, type Env struct, Usage func(*Env, io.Writer)
 pkg cmdline, type Env struct, Vars map[string]string
 pkg cmdline, type ErrExitCode int
 pkg cmdline, type Runner interface { Run }
diff --git a/cmdline/cmdline.go b/cmdline/cmdline.go
index da393e1..1286f55 100644
--- a/cmdline/cmdline.go
+++ b/cmdline/cmdline.go
@@ -174,6 +174,23 @@
 	if err != nil {
 		return nil, nil, err
 	}
+	// Clear envvars that start with "CMDLINE_" when we're returning a
+	// user-specified runner, to avoid polluting the environment.  In particular
+	// CMDLINE_PREFIX and CMDLINE_FIRST_CALL are only meant to be passed to binary
+	// subcommands, and shouldn't be propagated through the user's runner.
+	switch runner.(type) {
+	case helpRunner, binaryRunner:
+		// The help and binary runners need the envvars to be set.
+	default:
+		for key, _ := range env.Vars {
+			if strings.HasPrefix(key, "CMDLINE_") {
+				delete(env.Vars, key)
+				if err := os.Unsetenv(key); err != nil {
+					return nil, nil, err
+				}
+			}
+		}
+	}
 	return runner, args, nil
 }
 
@@ -273,12 +290,12 @@
 }
 
 func pathName(prefix string, path []*Command) string {
-	name := path[0].Name
-	for _, cmd := range path[1:] {
-		name += " " + cmd.Name
-	}
-	if prefix != "" {
-		return prefix + " " + name
+	name := prefix
+	for _, cmd := range path {
+		if name != "" {
+			name += " "
+		}
+		name += cmd.Name
 	}
 	return name
 }
@@ -374,7 +391,7 @@
 		defer func() {
 			flags.Init(cmd.Name, flag.ExitOnError)
 			flags.SetOutput(nil)
-			flags.Usage = func() { env.Usage(env.Stderr) }
+			flags.Usage = func() { env.Usage(env, env.Stderr) }
 		}()
 	}
 	if err := flags.Parse(args); err != nil {
@@ -447,12 +464,13 @@
 }
 
 func (b binaryRunner) Run(env *Env, args []string) error {
+	vars := envvar.CopyMap(env.Vars)
+	vars["CMDLINE_PREFIX"] = b.cmdPath
 	cmd := exec.Command(b.subCmd, args...)
 	cmd.Stdin = env.Stdin
 	cmd.Stdout = env.Stdout
 	cmd.Stderr = env.Stderr
-	cmd.Env = envvar.MapToSlice(env.Vars)
-	cmd.Env = append(cmd.Env, "CMDLINE_PREFIX="+b.cmdPath)
+	cmd.Env = envvar.MapToSlice(vars)
 	err := cmd.Run()
 	// Make sure we return the exit code from the binary, if it exited.
 	if exitError, ok := err.(*exec.ExitError); ok {
diff --git a/cmdline/cmdline_test.go b/cmdline/cmdline_test.go
index 69d687c..dcc446e 100644
--- a/cmdline/cmdline_test.go
+++ b/cmdline/cmdline_test.go
@@ -61,6 +61,11 @@
 	return nil
 }
 
+func runDumpEnv(env *Env, args []string) error {
+	fmt.Fprintln(env.Stdout, envvar.MapToSlice(env.Vars))
+	return nil
+}
+
 type testCase struct {
 	Args        []string
 	Vars        map[string]string
@@ -2469,10 +2474,10 @@
 		LookPath: true,
 		Children: []*Command{
 			&Command{
-				Runner: RunnerFunc(runHello),
-				Name:   "foo",
-				Short:  "Short description of command foo",
-				Long:   "Long description of command foo.",
+				Runner: RunnerFunc(runDumpEnv),
+				Name:   "dumpenv",
+				Short:  "Short description of command dumpenv",
+				Long:   "Long description of command dumpenv.",
 			},
 			&Command{
 				Runner: RunnerFunc(runHello),
@@ -2495,13 +2500,14 @@
    unlikely <command>
 
 The unlikely commands are:
-   foo         Short description of command foo
+   dumpenv     Short description of command dumpenv
    repeated    Repeated appears as both a child and as a binary
+   help        Display help for commands or topics
+The unlikely external commands are:
    exitcode    Short description of command exitcode
    flat        Short description of command flat
    foreign     No description available
    nested      Short description of command nested
-   help        Display help for commands or topics
 Run "unlikely help [command]" for command usage.
 
 The global flags are:
@@ -2522,13 +2528,14 @@
    unlikely <command>
 
 The unlikely commands are:
-   foo         Short description of command foo
+   dumpenv     Short description of command dumpenv
    repeated    Repeated appears as both a child and as a binary
+   help        Display help for commands or topics
+The unlikely external commands are:
    exitcode    Short description of command exitcode
    flat        Short description of command flat
    foreign     No description available
    nested      Short description of command nested
-   help        Display help for commands or topics
 Run "unlikely help [command]" for command usage.
 
 The global flags are:
@@ -2549,13 +2556,14 @@
    unlikely <command>
 
 The unlikely commands are:
-   foo         Short description of command foo
+   dumpenv     Short description of command dumpenv
    repeated    Repeated appears as both a child and as a binary
+   help        Display help for commands or topics
+The unlikely external commands are:
    exitcode    Short description of command exitcode
    flat        Short description of command flat
    foreign     No description available
    nested      Short description of command nested
-   help        Display help for commands or topics
 Run "unlikely help [command]" for command usage.
 
 The global flags are:
@@ -2564,12 +2572,12 @@
  -global2=0
    global test flag 2
 ================================================================================
-Unlikely foo - Short description of command foo
+Unlikely dumpenv - Short description of command dumpenv
 
-Long description of command foo.
+Long description of command dumpenv.
 
 Usage:
-   unlikely foo
+   unlikely dumpenv
 ================================================================================
 Unlikely repeated - Repeated appears as both a child and as a binary
 
@@ -2578,43 +2586,6 @@
 Usage:
    unlikely repeated
 ================================================================================
-Unlikely exitcode - Short description of command exitcode
-
-Long description of command exitcode.
-
-Usage:
-   unlikely exitcode [args]
-
-[args] are ignored
-================================================================================
-Unlikely flat - Short description of command flat
-
-Long description of command flat.
-
-Usage:
-   unlikely flat [args]
-
-[args] are ignored
-================================================================================
-Unlikely foreign - No description available
-================================================================================
-Unlikely nested - Short description of command nested
-
-Long description of command nested.
-
-Usage:
-   unlikely nested <command>
-
-The unlikely nested commands are:
-   child       Short description of command child
-================================================================================
-Unlikely nested child - Short description of command child
-
-Long description of command child.
-
-Usage:
-   unlikely nested child
-================================================================================
 Unlikely help - Display help for commands or topics
 
 Help with no args displays the usage of the parent command.
@@ -2639,6 +2610,43 @@
    Format output to this target width in runes, or unlimited if width < 0.
    Defaults to the terminal width if available.  Override the default by setting
    the CMDLINE_WIDTH environment variable.
+================================================================================
+Unlikely exitcode - Short description of command exitcode
+
+Long description of command exitcode.
+
+Usage:
+   unlikely exitcode [args]
+
+[args] are ignored
+================================================================================
+Unlikely flat - Short description of command flat
+
+Long description of command flat.
+
+Usage:
+   unlikely flat [args]
+
+[args] are ignored
+================================================================================
+Unlikely foreign - No description available
+================================================================================
+Unlikely nested - Short description of command nested
+
+Long description of command nested.
+
+Usage:
+   unlikely nested <command>
+
+The unlikely nested commands are:
+   child       Short description of command child
+================================================================================
+Unlikely nested child - Short description of command child
+
+Long description of command child.
+
+Usage:
+   unlikely nested child
 `,
 		},
 		{
@@ -2652,13 +2660,14 @@
    unlikely <command>
 
 The unlikely commands are:
-   foo         Short description of command foo
+   dumpenv     Short description of command dumpenv
    repeated    Repeated appears as both a child and as a binary
+   help        Display help for commands or topics
+The unlikely external commands are:
    exitcode    Short description of command exitcode
    flat        Short description of command flat
    foreign     No description available
    nested      Short description of command nested
-   help        Display help for commands or topics
 
 The global flags are:
  -global1=
@@ -2666,12 +2675,12 @@
  -global2=0
    global test flag 2
 
-Unlikely foo - Short description of command foo
+Unlikely dumpenv - Short description of command dumpenv
 
-Long description of command foo.
+Long description of command dumpenv.
 
 Usage:
-   unlikely foo
+   unlikely dumpenv
 
 Unlikely repeated - Repeated appears as both a child and as a binary
 
@@ -2680,6 +2689,31 @@
 Usage:
    unlikely repeated
 
+Unlikely help - Display help for commands or topics
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+   unlikely help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The unlikely help flags are:
+ -style=compact
+   The formatting style for help output:
+      compact - Good for compact cmdline output.
+      full    - Good for cmdline output, shows all global flags.
+      godoc   - Good for godoc processing.
+   Override the default by setting the CMDLINE_STYLE environment variable.
+ -width=<terminal width>
+   Format output to this target width in runes, or unlimited if width < 0.
+   Defaults to the terminal width if available.  Override the default by setting
+   the CMDLINE_WIDTH environment variable.
+
 Unlikely exitcode - Short description of command exitcode
 
 Long description of command exitcode.
@@ -2716,31 +2750,6 @@
 
 Usage:
    unlikely nested child
-
-Unlikely help - Display help for commands or topics
-
-Help with no args displays the usage of the parent command.
-
-Help with args displays the usage of the specified sub-command or help topic.
-
-"help ..." recursively displays help for all commands and topics.
-
-Usage:
-   unlikely help [flags] [command/topic ...]
-
-[command/topic ...] optionally identifies a specific sub-command or help topic.
-
-The unlikely help flags are:
- -style=compact
-   The formatting style for help output:
-      compact - Good for compact cmdline output.
-      full    - Good for cmdline output, shows all global flags.
-      godoc   - Good for godoc processing.
-   Override the default by setting the CMDLINE_STYLE environment variable.
- -width=<terminal width>
-   Format output to this target width in runes, or unlimited if width < 0.
-   Defaults to the terminal width if available.  Override the default by setting
-   the CMDLINE_WIDTH environment variable.
 `,
 		},
 		{
@@ -2764,6 +2773,36 @@
 `,
 		},
 		{
+			Args: []string{"nested", "child"},
+			Vars: map[string]string{
+				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
+			},
+			Err: errUsageStr,
+			Stderr: `ERROR: wombats!
+
+Long description of command child.
+
+Usage:
+   unlikely nested child
+
+The global flags are:
+ -metadata=<just specify -metadata to activate>
+   Displays metadata for the program and exits.
+`,
+		},
+		{
+			Args:   []string{"dumpenv"},
+			Vars:   map[string]string{"A": "a", "B": "b", "CMDLINE_PREFIX": "abc"},
+			Stdout: "[A=a B=b]\n",
+		},
+		{
+			Args: []string{"repeated"},
+			Vars: map[string]string{
+				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
+			},
+			Stdout: "Hello\n",
+		},
+		{
 			Args: []string{"exitcode"},
 			Vars: map[string]string{
 				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
diff --git a/cmdline/env.go b/cmdline/env.go
index 624fbff..a669fd3 100644
--- a/cmdline/env.go
+++ b/cmdline/env.go
@@ -36,39 +36,35 @@
 
 	// Usage is a function that prints usage information to w.  Typically set by
 	// calls to Main or Parse to print usage of the leaf command.
-	Usage func(w io.Writer)
+	Usage func(env *Env, w io.Writer)
 }
 
 // UsageErrorf prints the error message represented by the printf-style format
 // and args, followed by the output of the Usage function.  Returns ErrUsage to
 // make it easy to use from within the Runner.Run function.
 func (e *Env) UsageErrorf(format string, args ...interface{}) error {
-	return usageErrorf(e.Stderr, e.Usage, format, args...)
+	return usageErrorf(e, e.Usage, format, args...)
 }
 
 // Clone creates a deep copy of Env.
 func (e *Env) clone() *Env {
-	env := &Env{
+	return &Env{
 		Stdin:  e.Stdin,
 		Stdout: e.Stdout,
 		Stderr: e.Stderr,
-		Vars:   map[string]string{},
+		Vars:   envvar.CopyMap(e.Vars),
 		Usage:  e.Usage,
 	}
-	for key, value := range e.Vars {
-		env.Vars[key] = value
-	}
-	return env
 }
 
-func usageErrorf(w io.Writer, usage func(io.Writer), format string, args ...interface{}) error {
-	fmt.Fprint(w, "ERROR: ")
-	fmt.Fprintf(w, format, args...)
-	fmt.Fprint(w, "\n\n")
+func usageErrorf(env *Env, usage func(*Env, io.Writer), format string, args ...interface{}) error {
+	fmt.Fprint(env.Stderr, "ERROR: ")
+	fmt.Fprintf(env.Stderr, format, args...)
+	fmt.Fprint(env.Stderr, "\n\n")
 	if usage != nil {
-		usage(w)
+		usage(env, env.Stderr)
 	} else {
-		fmt.Fprint(w, "usage error\n")
+		fmt.Fprint(env.Stderr, "usage error\n")
 	}
 	return ErrUsage
 }
diff --git a/cmdline/env_test.go b/cmdline/env_test.go
index ba50535..1c21d55 100644
--- a/cmdline/env_test.go
+++ b/cmdline/env_test.go
@@ -10,15 +10,15 @@
 	"testing"
 )
 
-func writeFunc(s string) func(io.Writer) {
-	return func(w io.Writer) { w.Write([]byte(s)) }
+func writeFunc(s string) func(*Env, io.Writer) {
+	return func(_ *Env, w io.Writer) { w.Write([]byte(s)) }
 }
 
 func TestEnvUsageErrorf(t *testing.T) {
 	tests := []struct {
 		format string
 		args   []interface{}
-		usage  func(io.Writer)
+		usage  func(*Env, io.Writer)
 		want   string
 	}{
 		{"", nil, nil, "ERROR: \n\nusage error\n"},
diff --git a/cmdline/help.go b/cmdline/help.go
index af75600..9619e5f 100644
--- a/cmdline/help.go
+++ b/cmdline/help.go
@@ -29,42 +29,47 @@
 
 func makeHelpRunner(path []*Command, env *Env) helpRunner {
 	return helpRunner{path, &helpConfig{
-		env:   env,
-		style: env.style(),
-		width: env.width(),
+		style:     env.style(),
+		width:     env.width(),
+		prefix:    env.prefix(),
+		firstCall: env.firstCall(),
 	}}
 }
 
 // helpConfig holds configuration data for help.  The style and width may be
 // overriden by flags if the command returned by newCommand is parsed.
 type helpConfig struct {
-	env   *Env
-	style style
-	width int
+	style     style
+	width     int
+	prefix    string
+	firstCall bool
 }
 
 // Run implements the Runner interface method.
 func (h helpRunner) Run(env *Env, args []string) error {
 	w := textutil.NewUTF8LineWriter(env.Stdout, h.width)
 	defer w.Flush()
-	return runHelp(w, env.Stderr, args, h.rootPath, h.helpConfig)
+	return runHelp(w, env, args, h.rootPath, h.helpConfig)
 }
 
 // usageFunc is used as the implementation of the Env.Usage function.
-func (h helpRunner) usageFunc(writer io.Writer) {
+func (h helpRunner) usageFunc(env *Env, writer io.Writer) {
 	w := textutil.NewUTF8LineWriter(writer, h.width)
-	usage(w, h.rootPath, h.helpConfig, h.env.firstCall())
+	usage(w, env, h.rootPath, h.helpConfig, h.helpConfig.firstCall)
 	w.Flush()
 }
 
-const helpName = "help"
+const (
+	helpName  = "help"
+	helpShort = "Display help for commands or topics"
+)
 
 // newCommand returns a new help command that uses h as its Runner.
 func (h helpRunner) newCommand() *Command {
 	help := &Command{
 		Runner: h,
 		Name:   helpName,
-		Short:  "Display help for commands or topics",
+		Short:  helpShort,
 		Long: `
 Help with no args displays the usage of the parent command.
 
@@ -97,37 +102,38 @@
 }
 
 // runHelp implements the run-time behavior of the help command.
-func runHelp(w *textutil.LineWriter, stderr io.Writer, args []string, path []*Command, config *helpConfig) error {
+func runHelp(w *textutil.LineWriter, env *Env, args []string, path []*Command, config *helpConfig) error {
 	if len(args) == 0 {
-		usage(w, path, config, config.env.firstCall())
+		usage(w, env, path, config, config.firstCall)
 		return nil
 	}
 	if args[0] == "..." {
-		usageAll(w, path, config, config.env.firstCall())
+		usageAll(w, env, path, config, config.firstCall)
 		return nil
 	}
 	// Look for matching children.
-	cmd, subName, subArgs := path[len(path)-1], args[0], args[1:]
+	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
+	subName, subArgs := args[0], args[1:]
 	for _, child := range cmd.Children {
 		if child.Name == subName {
-			return runHelp(w, stderr, subArgs, append(path, child), config)
+			return runHelp(w, env, subArgs, append(path, child), config)
 		}
 	}
 	if helpName == subName {
 		help := helpRunner{path, config}.newCommand()
-		return runHelp(w, stderr, subArgs, append(path, help), config)
+		return runHelp(w, env, subArgs, append(path, help), config)
 	}
 	if cmd.LookPath {
 		// Look for a matching executable in PATH.
-		subCmd := cmd.Name + "-" + subName
-		if lookPath(subCmd, config.env.pathDirs()) {
-			runner := binaryRunner{subCmd, pathName(config.env.prefix(), path)}
-			env := config.env.clone()
-			env.Vars["CMDLINE_STYLE"] = config.style.String()
+		extName := cmd.Name + "-" + subName
+		if lookPath(extName, env.pathDirs()) {
+			runner := binaryRunner{extName, cmdPath}
+			envCopy := env.clone()
+			envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
 			if len(subArgs) == 0 {
-				return runner.Run(env, []string{"-help"})
+				return runner.Run(envCopy, []string{"-help"})
 			}
-			return runner.Run(env, append([]string{helpName}, subArgs...))
+			return runner.Run(envCopy, append([]string{helpName}, subArgs...))
 		}
 	}
 	// Look for matching topic.
@@ -138,7 +144,7 @@
 		}
 	}
 	fn := helpRunner{path, config}.usageFunc
-	return usageErrorf(stderr, fn, "%s: unknown command or topic %q", pathName(config.env.prefix(), path), subName)
+	return usageErrorf(env, fn, "%s: unknown command or topic %q", cmdPath, subName)
 }
 
 func godocHeader(path, short string) string {
@@ -208,24 +214,28 @@
 }
 
 // usageAll prints usage recursively via DFS from the path onward.
-func usageAll(w *textutil.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
-	cmd, cmdPath := path[len(path)-1], pathName(config.env.prefix(), path)
-	usage(w, path, config, firstCall)
+func usageAll(w *textutil.LineWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
+	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
+	usage(w, env, path, config, firstCall)
 	for _, child := range cmd.Children {
-		usageAll(w, append(path, child), config, false)
+		usageAll(w, env, append(path, child), config, false)
+	}
+	if firstCall && needsHelpChild(cmd) {
+		help := helpRunner{path, config}.newCommand()
+		usageAll(w, env, append(path, help), config, false)
 	}
 	if cmd.LookPath {
-		subCmds := lookPathAll(cmd.Name, config.env.pathDirs(), cmd.subNames())
-		for _, subCmd := range subCmds {
-			runner := binaryRunner{subCmd, cmdPath}
+		extNames := lookPathAll(cmd.Name, env.pathDirs(), cmd.subNames())
+		for _, extName := range extNames {
+			runner := binaryRunner{extName, cmdPath}
 			var buffer bytes.Buffer
-			env := config.env.clone()
-			env.Stdout = &buffer
-			env.Stderr = &buffer
-			env.Vars["CMDLINE_FIRST_CALL"] = "1"
-			env.Vars["CMDLINE_STYLE"] = config.style.String()
-			if err := runner.Run(env, []string{helpName, "..."}); err == nil {
-				// The binary subcommand supports "help".
+			envCopy := env.clone()
+			envCopy.Stdout = &buffer
+			envCopy.Stderr = &buffer
+			envCopy.Vars["CMDLINE_FIRST_CALL"] = "false"
+			envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
+			if err := runner.Run(envCopy, []string{helpName, "..."}); err == nil {
+				// The external child supports "help".
 				if config.style == styleGoDoc {
 					// The textutil package will discard any leading empty lines
 					// produced by the child process output, so we need to
@@ -236,8 +246,8 @@
 				continue
 			}
 			buffer.Reset()
-			if err := runner.Run(env, []string{"-help"}); err == nil {
-				// The binary subcommand supports "-help".
+			if err := runner.Run(envCopy, []string{"-help"}); err == nil {
+				// The external child supports "-help".
 				if config.style == styleGoDoc {
 					// The textutil package will discard any leading empty lines
 					// produced by the child process output, so we need to
@@ -247,15 +257,11 @@
 				fmt.Fprint(w, buffer.String())
 				continue
 			}
-			// The binary subcommand does not support "help" or "-help".
+			// The external child does not support "help" or "-help".
 			lineBreak(w, config.style)
-			fmt.Fprintln(w, godocHeader(cmdPath+" "+strings.TrimPrefix(subCmd, cmd.Name+"-"), missingDescription))
+			fmt.Fprintln(w, godocHeader(cmdPath+" "+strings.TrimPrefix(extName, cmd.Name+"-"), missingDescription))
 		}
 	}
-	if firstCall && needsHelpChild(cmd) {
-		help := helpRunner{path, config}.newCommand()
-		usageAll(w, append(path, help), config, false)
-	}
 	for _, topic := range cmd.Topics {
 		lineBreak(w, config.style)
 		w.ForceVerbatim(true)
@@ -269,8 +275,8 @@
 // usage prints the usage of the last command in path to w.  The bool firstCall
 // is set to false when printing usage for multiple commands, and is used to
 // avoid printing redundant information (e.g. help command, global flags).
-func usage(w *textutil.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
-	cmd, cmdPath := path[len(path)-1], pathName(config.env.prefix(), path)
+func usage(w *textutil.LineWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
+	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
 	if config.style == styleShort {
 		fmt.Fprintln(w, cmd.Short)
 		return
@@ -297,68 +303,66 @@
 			fmt.Fprintln(w, cmdPathF)
 		}
 	}
-	var subCmds []string
+	var extChildren []string
 	if cmd.LookPath {
-		subCmds = lookPathAll(cmd.Name, config.env.pathDirs(), cmd.subNames())
+		extChildren = lookPathAll(cmd.Name, env.pathDirs(), cmd.subNames())
 	}
-	hasSubcommands := len(subCmds) > 0 || len(cmd.Children) > 0
+	hasSubcommands := len(cmd.Children) > 0 || len(extChildren) > 0
 	if hasSubcommands {
 		fmt.Fprintln(w, cmdPathF, "<command>")
+		fmt.Fprintln(w)
 	}
-	// Compute the name width.
+	printShort := func(width int, name, short string) {
+		fmt.Fprintf(w, "%-[1]*[2]s %[3]s", width, name, short)
+		w.Flush()
+	}
 	const minNameWidth = 11
 	nameWidth := minNameWidth
 	for _, child := range cmd.Children {
-		if len(child.Name) > nameWidth {
-			nameWidth = len(child.Name)
+		if w := len(child.Name); w > nameWidth {
+			nameWidth = w
 		}
 	}
-	for _, subCmd := range subCmds {
-		length := len(strings.TrimPrefix(subCmd, cmd.Name+"-"))
-		if length > nameWidth {
-			nameWidth = length
+	for _, ext := range extChildren {
+		if w := len(strings.TrimPrefix(ext, cmd.Name+"-")); w > nameWidth {
+			nameWidth = w
 		}
 	}
-	// Command header.
-	if hasSubcommands {
-		fmt.Fprintln(w)
+	// Built-in commands.
+	if len(cmd.Children) > 0 {
+		w.SetIndents()
 		fmt.Fprintln(w, "The", cmdPath, "commands are:")
 		// Print as a table with aligned columns Name and Short.
 		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
-	}
-	printShort := func(name, short string) {
-		fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, name, short)
-		w.Flush()
-	}
-	// Built-in subcommands.
-	if len(cmd.Children) > 0 {
 		for _, child := range cmd.Children {
-			printShort(child.Name, child.Short)
+			printShort(nameWidth, child.Name, child.Short)
+		}
+		// Default help command.
+		if firstCall && needsHelpChild(cmd) {
+			printShort(nameWidth, helpName, helpShort)
 		}
 	}
-	// Binary subcommands.
-	if len(subCmds) > 0 {
-		for _, subCmd := range subCmds {
-			runner := binaryRunner{subCmd, cmdPath}
+	// External commands.
+	if len(extChildren) > 0 {
+		w.SetIndents()
+		fmt.Fprintln(w, "The", cmdPath, "external commands are:")
+		// Print as a table with aligned columns Name and Short.
+		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
+		for _, ext := range extChildren {
+			runner := binaryRunner{ext, cmdPath}
 			var buffer bytes.Buffer
-			env := config.env.clone()
-			env.Stdout = &buffer
-			env.Stderr = &buffer
-			env.Vars["CMDLINE_STYLE"] = "short"
-			if err := runner.Run(env, []string{"-help"}); err == nil {
-				// The binary subcommand supports "-help".
-				printShort(strings.TrimPrefix(subCmd, cmd.Name+"-"), buffer.String())
-			} else {
-				// The binary subcommand does not support "-help".
-				printShort(strings.TrimPrefix(subCmd, cmd.Name+"-"), missingDescription)
+			envCopy := env.clone()
+			envCopy.Stdout = &buffer
+			envCopy.Stderr = &buffer
+			envCopy.Vars["CMDLINE_STYLE"] = "short"
+			short := missingDescription
+			if err := runner.Run(envCopy, []string{"-help"}); err == nil {
+				// The external child supports "-help".
+				short = buffer.String()
 			}
+			printShort(nameWidth, strings.TrimPrefix(ext, cmd.Name+"-"), short)
 		}
 	}
-	// Default help command.
-	if firstCall && needsHelpChild(cmd) {
-		help := helpRunner{path, config}.newCommand()
-		printShort(help.Name, help.Short)
-	}
 	// Command footer.
 	if hasSubcommands {
 		w.SetIndents()
@@ -377,15 +381,14 @@
 		fmt.Fprintln(w, "The", cmdPath, "additional help topics are:")
 		nameWidth := minNameWidth
 		for _, topic := range cmd.Topics {
-			if len(topic.Name) > nameWidth {
-				nameWidth = len(topic.Name)
+			if w := len(topic.Name); w > nameWidth {
+				nameWidth = w
 			}
 		}
 		// Print as a table with aligned columns Name and Short.
 		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
 		for _, topic := range cmd.Topics {
-			fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, topic.Name, topic.Short)
-			w.Flush()
+			printShort(nameWidth, topic.Name, topic.Short)
 		}
 		w.SetIndents()
 		if firstCall && config.style != styleGoDoc {
@@ -396,7 +399,7 @@
 }
 
 func flagsUsage(w *textutil.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
-	cmd, cmdPath := path[len(path)-1], pathName(config.env.prefix(), path)
+	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
 	// Flags.
 	if countFlags(&cmd.Flags, nil, true) > 0 {
 		fmt.Fprintln(w)
@@ -433,7 +436,7 @@
 		fullhelp := fmt.Sprintf(`Run "%s help -style=full" to show all global flags.`, cmdPath)
 		if len(cmd.Children) == 0 {
 			if len(path) > 1 {
-				parentPath := pathName(config.env.prefix(), path[:len(path)-1])
+				parentPath := pathName(config.prefix, path[:len(path)-1])
 				fullhelp = fmt.Sprintf(`Run "%s help -style=full %s" to show all global flags.`, parentPath, cmd.Name)
 			} else {
 				fullhelp = fmt.Sprintf(`Run "CMDLINE_STYLE=full %s -help" to show all global flags.`, cmdPath)
diff --git a/cmdline/testdata/gendoc.go b/cmdline/testdata/gendoc.go
index 5a9f59c..266de4b 100644
--- a/cmdline/testdata/gendoc.go
+++ b/cmdline/testdata/gendoc.go
@@ -2,22 +2,25 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Command gendoc can be used for generating detailed godoc comments for
-// cmdline-based tools.  The user specifies the cmdline-based tool source file
-// directory <dir> using the first command-line argument and gendoc executes the
-// tool with flags that generate detailed godoc comment and output it to
-// <dir>/doc.go.  If more than one command-line argument is provided, they are
-// passed through to the tool the gendoc executes.
+// Command gendoc generates godoc comments describing the usage of tools based
+// on the cmdline package.
 //
-// NOTE: The reason this command is located under a testdata directory is to
-// enforce its idiomatic use through "go run <path>/testdata/gendoc.go <dir>
-// [args]".
+// Usage:
+//   go run gendoc.go [flags] <pkg> [args]
 //
-// NOTE: The gendoc command itself is not based on the cmdline library to avoid
+// <pkg> is the package path for the tool.
+//
+// [args] are the arguments to pass to the tool to produce usage output.  If no
+// args are given, runs "<tool> help ..."
+//
+// The reason this command is located under a testdata directory is to enforce
+// its idiomatic use via "go run".
+//
+// The gendoc command itself is not based on the cmdline library to avoid
 // non-trivial bootstrapping.  In particular, if the compilation of gendoc
 // requires GOPATH to contain the vanadium Go workspaces, then running the
-// gendoc command requires the jiri tool, which in turn may depend on the
-// gendoc command.
+// gendoc command requires the jiri tool, which in turn may depend on the gendoc
+// command.
 package main
 
 import (
@@ -32,10 +35,18 @@
 	"strings"
 )
 
-var flagTags string
+var (
+	flagEnv     string
+	flagInstall string
+	flagOut     string
+	flagTags    string
+)
 
 func main() {
-	flag.StringVar(&flagTags, "tags", "", "Tags for go build, also added as build constraints in the generated doc.go.")
+	flag.StringVar(&flagEnv, "env", "os", `Environment variables to set before running command.  If "os", grabs vars from the underlying OS.  If empty, doesn't set any vars.  Otherwise vars are expected to be comma-separated entries of the form KEY1=VALUE1,KEY2=VALUE2,...`)
+	flag.StringVar(&flagInstall, "install", "", "Comma separated list of packages to install before running command.  All commands that are built will be on the PATH.")
+	flag.StringVar(&flagOut, "out", "./doc.go", "Path to the output file.")
+	flag.StringVar(&flagTags, "tags", "", "Tags for go build, also added as build constraints in the generated output file.")
 	flag.Parse()
 	if err := generate(flag.Args()); err != nil {
 		fmt.Fprintln(os.Stderr, err)
@@ -45,50 +56,53 @@
 
 func generate(args []string) error {
 	if got, want := len(args), 1; got < want {
-		return fmt.Errorf("gendoc requires at least one argument\nusage: gendoc <dir> [args]")
+		return fmt.Errorf("gendoc requires at least one argument\nusage: gendoc <pkg> [args]")
 	}
 	pkg, args := args[0], args[1:]
 
 	// Find out the binary name from the pkg name.
 	var listOut bytes.Buffer
-	listCmd := exec.Command("go", "list")
+	listCmd := exec.Command("go", "list", pkg)
 	listCmd.Stdout = &listOut
 	if err := listCmd.Run(); err != nil {
 		return fmt.Errorf("%q failed: %v\n%v\n", strings.Join(listCmd.Args, " "), err, listOut.String())
 	}
 	binName := filepath.Base(strings.TrimSpace(listOut.String()))
 
-	// Install the gendoc binary in a temporary folder.
+	// Install all packages in a temporary directory.
 	tmpDir, err := ioutil.TempDir("", "")
 	if err != nil {
 		return fmt.Errorf("TempDir() failed: %v", err)
 	}
 	defer os.RemoveAll(tmpDir)
-	gendocBin := filepath.Join(tmpDir, binName)
-	env := environ()
-	env = append(env, "GOBIN="+tmpDir)
-	installArgs := []string{"go", "install", "-tags=" + flagTags, pkg}
-	installCmd := exec.Command("jiri", installArgs...)
-	installCmd.Env = env
-	if err := installCmd.Run(); err != nil {
-		return fmt.Errorf("%q failed: %v\n", strings.Join(installCmd.Args, " "), err)
+	pkgs := []string{pkg}
+	if flagInstall != "" {
+		pkgs = append(pkgs, strings.Split(flagInstall, ",")...)
+	}
+	for _, installPkg := range pkgs {
+		installArgs := []string{"go", "install", "-tags=" + flagTags, installPkg}
+		installCmd := exec.Command("jiri", installArgs...)
+		installCmd.Env = append(os.Environ(), "GOBIN="+tmpDir)
+		if err := installCmd.Run(); err != nil {
+			return fmt.Errorf("%q failed: %v\n", strings.Join(installCmd.Args, " "), err)
+		}
 	}
 
-	// Use it to generate the documentation.
-	var tagsConstraint string
-	if flagTags != "" {
-		tagsConstraint = fmt.Sprintf("// +build %s\n\n", flagTags)
-	}
+	// Run the binary to generate documentation.
 	var out bytes.Buffer
 	if len(args) == 0 {
 		args = []string{"help", "..."}
 	}
-	runCmd := exec.Command(gendocBin, args...)
+	runCmd := exec.Command(filepath.Join(tmpDir, binName), args...)
 	runCmd.Stdout = &out
-	runCmd.Env = environ()
+	runCmd.Env = runEnviron(tmpDir)
 	if err := runCmd.Run(); err != nil {
 		return fmt.Errorf("%q failed: %v\n%v\n", strings.Join(runCmd.Args, " "), err, out.String())
 	}
+	var tagsConstraint string
+	if flagTags != "" {
+		tagsConstraint = fmt.Sprintf("// +build %s\n\n", flagTags)
+	}
 	doc := fmt.Sprintf(`// Copyright 2015 The Vanadium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
@@ -101,8 +115,8 @@
 package main
 `, tagsConstraint, suppressParallelFlag(out.String()))
 
-	// Write the result to doc.go.
-	path, perm := filepath.Join(pkg, "doc.go"), os.FileMode(0644)
+	// Write the result to the output file.
+	path, perm := flagOut, os.FileMode(0644)
 	if err := ioutil.WriteFile(path, []byte(doc), perm); err != nil {
 		return fmt.Errorf("WriteFile(%v, %v) failed: %v\n", path, perm, err)
 	}
@@ -120,18 +134,28 @@
 	return pattern.ReplaceAllString(input, "$1<number of threads>")
 }
 
-// environ returns the environment variables to use when running the command to
-// retrieve full help information.
-func environ() []string {
-	var env []string
-	for _, e := range os.Environ() {
-		// Strip out all existing CMDLINE_* envvars to start with a clean slate.
-		// E.g. otherwise if CMDLINE_PREFIX is set, it'll taint all of the output.
-		if !strings.HasPrefix(e, "CMDLINE_") {
-			env = append(env, e)
-		}
+// runEnviron returns the environment variables to use when running the command
+// to retrieve full help information.
+func runEnviron(binDir string) []string {
+	// Never return nil, which signals exec.Command to use os.Environ.
+	in, out := strings.Split(flagEnv, ","), make([]string, 0)
+	if flagEnv == "os" {
+		in = os.Environ()
 	}
-	// We want the godoc style for our generated documentation.
-	env = append(env, "CMDLINE_STYLE=godoc")
-	return env
+	updatedPath := false
+	for _, e := range in {
+		if e == "" {
+			continue
+		}
+		if strings.HasPrefix(e, "PATH=") {
+			e = "PATH=" + binDir + string(os.PathListSeparator) + e[5:]
+			updatedPath = true
+		}
+		out = append(out, e)
+	}
+	if !updatedPath {
+		out = append(out, "PATH="+binDir)
+	}
+	out = append(out, "CMDLINE_STYLE=godoc")
+	return out
 }
diff --git a/cmdline/testdata/nested.go b/cmdline/testdata/nested.go
index fb57de2..a7a9c36 100644
--- a/cmdline/testdata/nested.go
+++ b/cmdline/testdata/nested.go
@@ -24,7 +24,7 @@
 }
 
 func runChild(env *cmdline.Env, _ []string) error {
-	return nil
+	return env.UsageErrorf("wombats!")
 }
 
 func main() {
diff --git a/envvar/.api b/envvar/.api
index 9446e8a..af7a167 100644
--- a/envvar/.api
+++ b/envvar/.api
@@ -1,3 +1,5 @@
+pkg envvar, func CopyMap(map[string]string) map[string]string
+pkg envvar, func CopySlice([]string) []string
 pkg envvar, func JoinKeyValue(string, string) string
 pkg envvar, func JoinTokens([]string, string) string
 pkg envvar, func MapToSlice(map[string]string) []string
diff --git a/envvar/envvar.go b/envvar/envvar.go
index 5157a88..f34df73 100644
--- a/envvar/envvar.go
+++ b/envvar/envvar.go
@@ -44,6 +44,11 @@
 	return merged
 }
 
+// CopyMap returns a copy of from, with empty keys dropped.
+func CopyMap(from map[string]string) map[string]string {
+	return MergeMaps(from)
+}
+
 // MergeSlices merges together slices, and returns a new slice with the merged
 // result.  If the same key appears more than once in a single input slice, or
 // in more than one input slice, the last one "wins"; the value is set based on
@@ -63,6 +68,13 @@
 	return MapToSlice(merged)
 }
 
+// CopySlice returns a copy of from, with empty keys dropped, and ordered by
+// key.  If the same key appears more than once the last one "wins"; the value
+// is set based on the last slice element containing that key.
+func CopySlice(from []string) []string {
+	return MergeSlices(from)
+}
+
 // MapToSlice converts from the map to the slice representation.  The returned
 // slice is in sorted order.
 func MapToSlice(from map[string]string) []string {
diff --git a/envvar/envvar_test.go b/envvar/envvar_test.go
index d991184..e778c53 100644
--- a/envvar/envvar_test.go
+++ b/envvar/envvar_test.go
@@ -82,18 +82,24 @@
 		if got, want := MergeSlices(slices...), test.Slice; !reflect.DeepEqual(got, want) {
 			t.Errorf("MergeSlices got %v, want %v", got, want)
 		}
-		// Test MergeMaps with a single map performs a copy.
-		mergedMap := MergeMaps(test.Map)
-		mergedMap["Z"] = "zzz"
-		if reflect.DeepEqual(mergedMap, test.Map) {
-			t.Errorf("MergeMaps(%v) failed copy semantics", mergedMap)
+		// Test CopyMap actually returns a copy.
+		copyMap := CopyMap(test.Map)
+		if !reflect.DeepEqual(copyMap, test.Map) {
+			t.Errorf("CopyMap got %v, want %v", copyMap, test.Map)
 		}
-		// TestMergeSlices with a single slice performs a copy.
-		mergedSlice := MergeSlices(test.Slice)
-		if len(mergedSlice) > 0 {
-			mergedSlice[0] = "Z=zzz"
-			if reflect.DeepEqual(mergedSlice, test.Slice) {
-				t.Errorf("MergeSlices(%v) failed copy semantics", mergedSlice)
+		copyMap["Z"] = "zzz"
+		if reflect.DeepEqual(copyMap, test.Map) {
+			t.Errorf("CopyMap(%v) failed copy semantics", copyMap)
+		}
+		// Test CopySlice actually returns a copy.
+		copySlice := CopySlice(test.Slice)
+		if len(copySlice) > 0 {
+			if !reflect.DeepEqual(copySlice, test.Slice) {
+				t.Errorf("CopySlice got %v, want %v", copySlice, test.Slice)
+			}
+			copySlice[0] = "Z=zzz"
+			if reflect.DeepEqual(copySlice, test.Slice) {
+				t.Errorf("CopySlice(%v) failed copy semantics", copySlice)
 			}
 		}
 	}