Merge "lib/cmdline: adding support for binary based subcommands"
diff --git a/buildinfo/.api b/buildinfo/.api
deleted file mode 100644
index e69de29..0000000
--- a/buildinfo/.api
+++ /dev/null
diff --git a/cmdline/.api b/cmdline/.api
index a0a8b60..583cc7e 100644
--- a/cmdline/.api
+++ b/cmdline/.api
@@ -14,6 +14,7 @@
 pkg cmdline, type Command struct, Children []*Command
 pkg cmdline, type Command struct, Flags flag.FlagSet
 pkg cmdline, type Command struct, Long string
+pkg cmdline, type Command struct, LookPath bool
 pkg cmdline, type Command struct, Name string
 pkg cmdline, type Command struct, Runner Runner
 pkg cmdline, type Command struct, Short string
diff --git a/cmdline/cmdline.go b/cmdline/cmdline.go
index 7b1ddbc..549ac5b 100644
--- a/cmdline/cmdline.go
+++ b/cmdline/cmdline.go
@@ -41,9 +41,11 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"strings"
 
-	_ "v.io/x/lib/metadata" // for the -v23.metadata flag
+	"v.io/x/lib/envvar"
+	_ "v.io/x/lib/metadata" // for the -metadata flag
 )
 
 // Command represents a single command in a command-line program.  A program
@@ -57,6 +59,7 @@
 	Flags    flag.FlagSet // Flags for the command.
 	ArgsName string       // Name of the args, shown in usage line.
 	ArgsLong string       // Long description of the args, shown in help.
+	LookPath bool         // Check for subcommands in PATH.
 
 	// Children of the command.
 	Children []*Command
@@ -157,7 +160,7 @@
 	path := []*Command{root}
 	env.Usage = makeHelpRunner(path, env).usageFunc
 	cleanTree(root)
-	if err := checkTreeInvariants(path); err != nil {
+	if err := checkTreeInvariants(path, env); err != nil {
 		return nil, nil, err
 	}
 	runner, args, err := root.parse(nil, env, args)
@@ -204,8 +207,8 @@
 	})
 }
 
-func checkTreeInvariants(path []*Command) error {
-	cmd, cmdPath := path[len(path)-1], pathName(path)
+func checkTreeInvariants(path []*Command, env *Env) error {
+	cmd, cmdPath := path[len(path)-1], pathName(env.prefix(), path)
 	// Check that the root name is non-empty.
 	if cmdPath == "" {
 		return fmt.Errorf(`CODE INVARIANT BROKEN; FIX YOUR CODE
@@ -255,24 +258,27 @@
 	}
 	// Check recursively for all children
 	for _, child := range cmd.Children {
-		if err := checkTreeInvariants(append(path, child)); err != nil {
+		if err := checkTreeInvariants(append(path, child), env); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func pathName(path []*Command) string {
+func pathName(prefix string, path []*Command) string {
 	name := path[0].Name
 	for _, cmd := range path[1:] {
 		name += " " + cmd.Name
 	}
+	if prefix != "" {
+		return prefix + " " + name
+	}
 	return name
 }
 
 func (cmd *Command) parse(path []*Command, env *Env, args []string) (Runner, []string, error) {
 	path = append(path, cmd)
-	cmdPath := pathName(path)
+	cmdPath := pathName(env.prefix(), path)
 	runHelp := makeHelpRunner(path, env)
 	env.Usage = runHelp.usageFunc
 	// Parse flags and retrieve the args remaining after the parse.
@@ -304,7 +310,14 @@
 			return runHelp.newCommand().parse(path, env, subArgs)
 		}
 	}
-	// No matching children, check various error cases.
+	if cmd.LookPath {
+		// Look for a matching executable in PATH.
+		subCmd := cmd.Name + "-" + subName
+		if lookPath(subCmd, env.pathDirs()) {
+			return binaryRunner{subCmd, cmdPath}, subArgs, nil
+		}
+	}
+	// No matching subcommands, check various error cases.
 	switch {
 	case cmd.Runner == nil:
 		return nil, nil, env.UsageErrorf("%s: unknown command %q", cmdPath, subName)
@@ -406,3 +419,58 @@
 	}
 	return 1
 }
+
+type binaryRunner struct {
+	subCmd  string
+	cmdPath string
+}
+
+func (b binaryRunner) Run(env *Env, args []string) error {
+	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)
+	return cmd.Run()
+}
+
+// lookPath returns a boolean that indicates whether executable <name>
+// can be found in any of the given directories.
+func lookPath(name string, dirs []string) bool {
+	for _, dir := range dirs {
+		fileInfos, err := ioutil.ReadDir(dir)
+		if err != nil {
+			continue
+		}
+		for _, fileInfo := range fileInfos {
+			if m := fileInfo.Mode(); !m.IsRegular() || (m&os.FileMode(0111)) == 0 {
+				continue
+			}
+			if fileInfo.Name() == name {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// lookPathAll returns a list of all executables found in the given
+// directories whose name starts with "<name>-".
+func lookPathAll(name string, dirs []string) (result []string) {
+	for _, dir := range dirs {
+		fileInfos, err := ioutil.ReadDir(dir)
+		if err != nil {
+			continue
+		}
+		for _, fileInfo := range fileInfos {
+			if m := fileInfo.Mode(); !m.IsRegular() || (m&os.FileMode(0111)) == 0 {
+				continue
+			}
+			if strings.HasPrefix(fileInfo.Name(), name+"-") {
+				result = append(result, fileInfo.Name())
+			}
+		}
+	}
+	return
+}
diff --git a/cmdline/cmdline_test.go b/cmdline/cmdline_test.go
index 0f2af2e..d2c56ae 100644
--- a/cmdline/cmdline_test.go
+++ b/cmdline/cmdline_test.go
@@ -9,6 +9,10 @@
 	"errors"
 	"flag"
 	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
 	"regexp"
 	"strings"
 	"testing"
@@ -2432,3 +2436,262 @@
 		t.Errorf("rstring got %v want %v", got, want)
 	}
 }
+
+func TestBinarySubcommand(t *testing.T) {
+	// Create a temporary directory for the binary subcommands.
+	tmpDir, err := ioutil.TempDir("", "cmdline-test")
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	// Add the temporary directory to PATH.
+	oldPath := os.Getenv("PATH")
+	defer os.Setenv("PATH", oldPath)
+	tokens := strings.Split(oldPath, string(os.PathListSeparator))
+	tokens = append([]string{tmpDir}, tokens...)
+	os.Setenv("PATH", strings.Join(tokens, string(os.PathListSeparator)))
+
+	// Build the binary subcommands.
+	for _, subCmd := range []string{"flat", "foreign", "nested"} {
+		cmd := exec.Command("go", "build", "-o", filepath.Join(tmpDir, "unlikely-"+subCmd), filepath.Join(".", "testdata", subCmd+".go"))
+		if out, err := cmd.CombinedOutput(); err != nil {
+			t.Fatalf("%v, %v", string(out), err)
+		}
+	}
+
+	// Create a command that uses these.
+	cmd := &Command{
+		Name:     "unlikely",
+		Short:    "Short description of command unlikely",
+		Long:     "Long description of command unlikely.",
+		LookPath: true,
+		Children: []*Command{
+			&Command{
+				Runner: RunnerFunc(runHello),
+				Name:   "foo",
+				Short:  "Short description of command foo",
+				Long:   "Long description of command foo.",
+			},
+		},
+	}
+
+	var tests = []testCase{
+		{
+			Args: []string{"-help"},
+			Vars: map[string]string{
+				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
+			},
+			Stdout: `Long description of command unlikely.
+
+Usage:
+   unlikely <command>
+
+The unlikely commands are:
+   foo         Short description of command foo
+   help        Display help for commands or topics
+   flat        Short description of command flat
+   foreign     No description available
+   nested      Short description of command nested
+Run "unlikely help [command]" for command usage.
+
+The global flags are:
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
+`,
+		},
+		{
+			Args: []string{"help"},
+			Vars: map[string]string{
+				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
+			},
+			Stdout: `Long description of command unlikely.
+
+Usage:
+   unlikely <command>
+
+The unlikely commands are:
+   foo         Short description of command foo
+   help        Display help for commands or topics
+   flat        Short description of command flat
+   foreign     No description available
+   nested      Short description of command nested
+Run "unlikely help [command]" for command usage.
+
+The global flags are:
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
+`,
+		},
+		{
+			Args: []string{"help", "..."},
+			Vars: map[string]string{
+				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
+			},
+			Stdout: `Long description of command unlikely.
+
+Usage:
+   unlikely <command>
+
+The unlikely commands are:
+   foo         Short description of command foo
+   help        Display help for commands or topics
+   flat        Short description of command flat
+   foreign     No description available
+   nested      Short description of command nested
+Run "unlikely help [command]" for command usage.
+
+The global flags are:
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
+================================================================================
+Unlikely foo - Short description of command foo
+
+Long description of command foo.
+
+Usage:
+   unlikely foo
+================================================================================
+Unlikely flat - Short description of command flat
+
+Long description of command flat.
+
+Usage:
+   unlikely flat
+================================================================================
+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.
+
+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=80
+   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.
+`,
+		},
+		{
+			Args: []string{"help", "-style=godoc", "..."},
+			Vars: map[string]string{
+				"PATH": strings.Join(tokens, string(os.PathListSeparator)),
+			},
+			Stdout: `Long description of command unlikely.
+
+Usage:
+   unlikely <command>
+
+The unlikely commands are:
+   foo         Short description of command foo
+   help        Display help for commands or topics
+   flat        Short description of command flat
+   foreign     No description available
+   nested      Short description of command nested
+
+The global flags are:
+ -global1=
+   global test flag 1
+ -global2=0
+   global test flag 2
+
+Unlikely foo - Short description of command foo
+
+Long description of command foo.
+
+Usage:
+   unlikely foo
+
+Unlikely flat - Short description of command flat
+
+Long description of command flat.
+
+Usage:
+   unlikely flat
+
+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.
+
+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.
+`,
+		},
+	}
+	runTestCases(t, cmd, tests)
+}
diff --git a/cmdline/env.go b/cmdline/env.go
index 77ff558..624fbff 100644
--- a/cmdline/env.go
+++ b/cmdline/env.go
@@ -9,6 +9,7 @@
 	"io"
 	"os"
 	"strconv"
+	"strings"
 
 	"v.io/x/lib/envvar"
 	"v.io/x/lib/textutil"
@@ -45,6 +46,21 @@
 	return usageErrorf(e.Stderr, e.Usage, format, args...)
 }
 
+// Clone creates a deep copy of Env.
+func (e *Env) clone() *Env {
+	env := &Env{
+		Stdin:  e.Stdin,
+		Stdout: e.Stdout,
+		Stderr: e.Stderr,
+		Vars:   map[string]string{},
+		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...)
@@ -76,6 +92,18 @@
 	return style
 }
 
+func (e *Env) prefix() string {
+	return e.Vars["CMDLINE_PREFIX"]
+}
+
+func (e *Env) pathDirs() []string {
+	return strings.Split(e.Vars["PATH"], string(os.PathListSeparator))
+}
+
+func (e *Env) firstCall() bool {
+	return e.Vars["CMDLINE_FIRST_CALL"] == ""
+}
+
 // style describes the formatting style for usage descriptions.
 type style int
 
@@ -83,6 +111,7 @@
 	styleCompact style = iota // Default style, good for compact cmdline output.
 	styleFull                 // Similar to compact but shows global flags.
 	styleGoDoc                // Style good for godoc processing.
+	styleShort                // Style good for displaying help of binary subcommands.
 )
 
 func (s *style) String() string {
@@ -93,6 +122,8 @@
 		return "full"
 	case styleGoDoc:
 		return "godoc"
+	case styleShort:
+		return "short"
 	default:
 		panic(fmt.Errorf("unhandled style %d", *s))
 	}
@@ -107,6 +138,8 @@
 		*s = styleFull
 	case "godoc":
 		*s = styleGoDoc
+	case "short":
+		*s = styleShort
 	default:
 		return fmt.Errorf("unknown style %q", value)
 	}
diff --git a/cmdline/help.go b/cmdline/help.go
index 178c02b..4eafa16 100644
--- a/cmdline/help.go
+++ b/cmdline/help.go
@@ -18,6 +18,8 @@
 	"v.io/x/lib/textutil"
 )
 
+const missingDescription = "No description available"
+
 // helpRunner is a Runner that implements the "help" functionality.  Help is
 // requested for the last command in rootPath, which must not be empty.
 type helpRunner struct {
@@ -26,12 +28,17 @@
 }
 
 func makeHelpRunner(path []*Command, env *Env) helpRunner {
-	return helpRunner{path, &helpConfig{env.style(), env.width()}}
+	return helpRunner{path, &helpConfig{
+		env:   env,
+		style: env.style(),
+		width: env.width(),
+	}}
 }
 
 // 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
 }
@@ -39,15 +46,14 @@
 // Run implements the Runner interface method.
 func (h helpRunner) Run(env *Env, args []string) error {
 	w := textutil.NewUTF8LineWriter(env.Stdout, h.width)
-	err := runHelp(w, env.Stderr, args, h.rootPath, h.helpConfig)
-	w.Flush()
-	return err
+	defer w.Flush()
+	return runHelp(w, env.Stderr, args, h.rootPath, h.helpConfig)
 }
 
 // usageFunc is used as the implementation of the Env.Usage function.
 func (h helpRunner) usageFunc(writer io.Writer) {
 	w := textutil.NewUTF8LineWriter(writer, h.width)
-	usage(w, h.rootPath, h.helpConfig, true)
+	usage(w, h.rootPath, h.helpConfig, h.env.firstCall())
 	w.Flush()
 }
 
@@ -93,11 +99,11 @@
 // 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 {
 	if len(args) == 0 {
-		usage(w, path, config, true)
+		usage(w, path, config, config.env.firstCall())
 		return nil
 	}
 	if args[0] == "..." {
-		usageAll(w, path, config, true)
+		usageAll(w, path, config, config.env.firstCall())
 		return nil
 	}
 	// Look for matching children.
@@ -111,6 +117,19 @@
 		help := helpRunner{path, config}.newCommand()
 		return runHelp(w, stderr, 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()
+			if len(subArgs) == 0 {
+				return runner.Run(env, []string{"-help"})
+			}
+			return runner.Run(env, append([]string{helpName}, subArgs...))
+		}
+	}
 	// Look for matching topic.
 	for _, topic := range cmd.Topics {
 		if topic.Name == subName {
@@ -119,7 +138,7 @@
 		}
 	}
 	fn := helpRunner{path, config}.usageFunc
-	return usageErrorf(stderr, fn, "%s: unknown command or topic %q", pathName(path), subName)
+	return usageErrorf(stderr, fn, "%s: unknown command or topic %q", pathName(config.env.prefix(), path), subName)
 }
 
 func godocHeader(path, short string) string {
@@ -190,18 +209,49 @@
 
 // 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(path)
-	if !firstCall {
-		lineBreak(w, config.style)
-		w.ForceVerbatim(true)
-		fmt.Fprintln(w, godocHeader(cmdPath, cmd.Short))
-		w.ForceVerbatim(false)
-		fmt.Fprintln(w)
-	}
+	cmd, cmdPath := path[len(path)-1], pathName(config.env.prefix(), path)
 	usage(w, path, config, firstCall)
 	for _, child := range cmd.Children {
 		usageAll(w, append(path, child), config, false)
 	}
+	if cmd.LookPath {
+		subCmds := lookPathAll(cmd.Name, config.env.pathDirs())
+		for _, subCmd := range subCmds {
+			runner := binaryRunner{subCmd, 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".
+				if config.style == styleGoDoc {
+					// The textutil package will discard any leading empty lines
+					// produced by the child process output, so we need to
+					// output it here.
+					fmt.Fprintln(w)
+				}
+				fmt.Fprint(w, buffer.String())
+				continue
+			}
+			buffer.Reset()
+			if err := runner.Run(env, []string{"-help"}); err == nil {
+				// The binary subcommand 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
+					// output it here.
+					fmt.Fprintln(w)
+				}
+				fmt.Fprint(w, buffer.String())
+				continue
+			}
+			// The binary subcommand does not support "help" or "-help".
+			lineBreak(w, config.style)
+			fmt.Fprintln(w, godocHeader(cmdPath+" "+strings.TrimPrefix(subCmd, cmd.Name+"-"), missingDescription))
+		}
+	}
 	if firstCall && needsHelpChild(cmd) {
 		help := helpRunner{path, config}.newCommand()
 		usageAll(w, append(path, help), config, false)
@@ -220,12 +270,27 @@
 // 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(path)
+	cmd, cmdPath := path[len(path)-1], pathName(config.env.prefix(), path)
+	if config.style == styleShort {
+		fmt.Fprintln(w, cmd.Short)
+		return
+	}
 	children := cmd.Children
 	if firstCall && needsHelpChild(cmd) {
 		help := helpRunner{path, config}.newCommand()
 		children = append(children, help)
 	}
+	var subCmds []string
+	if cmd.LookPath {
+		subCmds = lookPathAll(cmd.Name, config.env.pathDirs())
+	}
+	if !firstCall {
+		lineBreak(w, config.style)
+		w.ForceVerbatim(true)
+		fmt.Fprintln(w, godocHeader(cmdPath, cmd.Short))
+		w.ForceVerbatim(false)
+		fmt.Fprintln(w)
+	}
 	fmt.Fprintln(w, cmd.Long)
 	fmt.Fprintln(w)
 	// Usage line.
@@ -241,26 +306,60 @@
 			fmt.Fprintln(w, cmdPathF)
 		}
 	}
-	if len(children) > 0 {
+	hasSubcommands := len(subCmds) > 0 || len(children) > 0
+	if hasSubcommands {
 		fmt.Fprintln(w, cmdPathF, "<command>")
 	}
-	// Commands.
+	// Compute the name width.
 	const minNameWidth = 11
-	if len(children) > 0 {
+	nameWidth := minNameWidth
+	for _, child := range children {
+		if len(child.Name) > nameWidth {
+			nameWidth = len(child.Name)
+		}
+	}
+	for _, subCmd := range subCmds {
+		length := len(strings.TrimPrefix(subCmd, cmd.Name+"-"))
+		if length > nameWidth {
+			nameWidth = length
+		}
+	}
+	// Command header.
+	if hasSubcommands {
 		fmt.Fprintln(w)
 		fmt.Fprintln(w, "The", cmdPath, "commands are:")
-		nameWidth := minNameWidth
-		for _, child := range children {
-			if len(child.Name) > nameWidth {
-				nameWidth = len(child.Name)
-			}
-		}
 		// Print as a table with aligned columns Name and Short.
 		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
+	}
+	// Built-in subcommands.
+	if len(children) > 0 {
 		for _, child := range children {
 			fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, child.Name, child.Short)
 			w.Flush()
 		}
+	}
+	// Binary subcommands.
+	if len(subCmds) > 0 {
+		for _, subCmd := range subCmds {
+			runner := binaryRunner{subCmd, 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".
+				fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, strings.TrimPrefix(subCmd, cmd.Name+"-"), buffer.String())
+				w.Flush()
+				continue
+			}
+			// The binary subcommand does not support "-help".
+			fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, strings.TrimPrefix(subCmd, cmd.Name+"-"), missingDescription)
+			w.Flush()
+		}
+	}
+	// Command footer.
+	if hasSubcommands {
 		w.SetIndents()
 		if firstCall && config.style != styleGoDoc {
 			fmt.Fprintf(w, "Run \"%s help [command]\" for command usage.\n", cmdPath)
@@ -296,7 +395,7 @@
 }
 
 func flagsUsage(w *textutil.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
-	cmd, cmdPath := path[len(path)-1], pathName(path)
+	cmd, cmdPath := path[len(path)-1], pathName(config.env.prefix(), path)
 	// Flags.
 	if countFlags(&cmd.Flags, nil, true) > 0 {
 		fmt.Fprintln(w)
@@ -333,7 +432,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(path[:len(path)-1])
+				parentPath := pathName(config.env.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/flat.go b/cmdline/testdata/flat.go
new file mode 100644
index 0000000..aa06085
--- /dev/null
+++ b/cmdline/testdata/flat.go
@@ -0,0 +1,25 @@
+// 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.
+
+package main
+
+import (
+	".."
+)
+
+// cmdFlat represents the flat command.
+var cmdFlat = &cmdline.Command{
+	Runner: cmdline.RunnerFunc(runFlat),
+	Name:   "flat",
+	Short:  "Short description of command flat",
+	Long:   "Long description of command flat.",
+}
+
+func runFlat(env *cmdline.Env, _ []string) error {
+	return nil
+}
+
+func main() {
+	cmdline.Main(cmdFlat)
+}
diff --git a/cmdline/testdata/foreign.go b/cmdline/testdata/foreign.go
new file mode 100644
index 0000000..45d71fb
--- /dev/null
+++ b/cmdline/testdata/foreign.go
@@ -0,0 +1,13 @@
+// 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.
+
+package main
+
+import (
+	"os"
+)
+
+func main() {
+	os.Exit(1)
+}
diff --git a/cmdline/testdata/nested.go b/cmdline/testdata/nested.go
new file mode 100644
index 0000000..cf9826c
--- /dev/null
+++ b/cmdline/testdata/nested.go
@@ -0,0 +1,34 @@
+// 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.
+
+package main
+
+import (
+	".."
+)
+
+// cmdNested represents the nested command.
+var cmdNested = &cmdline.Command{
+	Name:     "nested",
+	Short:    "Short description of command nested",
+	Long:     "Long description of command nested.",
+	LookPath: true,
+	Children: []*cmdline.Command{cmdChild},
+}
+
+// cmdChild represents the child command.
+var cmdChild = &cmdline.Command{
+	Runner: cmdline.RunnerFunc(runChild),
+	Name:   "child",
+	Short:  "Short description of command child",
+	Long:   "Long description of command child.",
+}
+
+func runChild(env *cmdline.Env, _ []string) error {
+	return nil
+}
+
+func main() {
+	cmdline.Main(cmdNested)
+}
diff --git a/metadata/metadata.go b/metadata/metadata.go
index 2510b42..167af58 100644
--- a/metadata/metadata.go
+++ b/metadata/metadata.go
@@ -38,8 +38,8 @@
 // The built-in metadata comes pre-populated with the Go architecture, operating
 // system and version.
 //
-// This package registers a flag -v23.metadata via an init function.  Setting
-// -v23.metadata on the command-line causes the program to dump metadata in the
+// This package registers a flag -metadata via an init function.  Setting
+// -metadata on the command-line causes the program to dump metadata in the
 // XML format and exit.
 package metadata
 
@@ -308,7 +308,7 @@
 	BuiltIn.Insert("go.OS", runtime.GOOS)
 	BuiltIn.Insert("go.Version", runtime.Version())
 
-	flag.Var(metadataFlag{}, "v23.metadata", "Displays metadata for the program and exits.")
+	flag.Var(metadataFlag{}, "metadata", "Displays metadata for the program and exits.")
 }
 
 // metadataFlag implements a flag that dumps the default metadata and exits the
@@ -316,7 +316,7 @@
 type metadataFlag struct{}
 
 func (metadataFlag) IsBoolFlag() bool { return true }
-func (metadataFlag) String() string   { return "<just specify -v23.metadata to activate>" }
+func (metadataFlag) String() string   { return "<just specify -metadata to activate>" }
 func (metadataFlag) Set(string) error {
 	fmt.Println(BuiltIn.String())
 	os.Exit(0)
diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go
index 5dbc47a..f9e2763 100644
--- a/metadata/metadata_test.go
+++ b/metadata/metadata_test.go
@@ -234,12 +234,12 @@
 }
 
 // TestInitAndFlag builds a test binary with some metadata, and invokes the
-// -v23.metadata flag to make sure it dumps the expected metadata.
+// -metadata flag to make sure it dumps the expected metadata.
 func TestInitAndFlag(t *testing.T) {
 	// Run the test binary.
 	const id, value = "zzzTestID", "abcdefg"
 	x := FromMap(map[string]string{id: value})
-	cmdRun := exec.Command("go", "run", "-ldflags="+LDFlag(x), "./testdata/testbin.go", "-v23.metadata")
+	cmdRun := exec.Command("go", "run", "-ldflags="+LDFlag(x), "./testdata/testbin.go", "-metadata")
 	outXML, err := cmdRun.CombinedOutput()
 	if err != nil {
 		t.Errorf("%v failed: %v\n%v", cmdRun.Args, err, outXML)