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)
}
}
}