github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cmdline/cmdline.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package cmdline implements a data-driven mechanism for writing command-line
     6  // programs with built-in support for help.
     7  //
     8  // Commands are linked together to form a command tree.  Since commands may be
     9  // arbitrarily nested within other commands, it's easy to create wrapper
    10  // programs that invoke existing commands.
    11  //
    12  // The syntax for each command-line program is:
    13  //
    14  //   command [flags] [subcommand [flags]]* [args]
    15  //
    16  // Each sequence of flags is associated with the command that immediately
    17  // precedes it.  Flags registered on flag.CommandLine are considered global
    18  // flags, and are allowed anywhere a command-specific flag is allowed.
    19  //
    20  // Pretty usage documentation is automatically generated, and accessible either
    21  // via the standard -h / -help flags from the Go flag package, or a special help
    22  // command.  The help command is automatically appended to commands that already
    23  // have at least one child, and don't already have a "help" child.  Commands
    24  // that do not have any children will exit with an error if invoked with the
    25  // arguments "help ..."; this behavior is relied on when generating recursive
    26  // help to distinguish between external subcommands with and without children.
    27  //
    28  // Pitfalls
    29  //
    30  // The cmdline package must be in full control of flag parsing.  Typically you
    31  // call cmdline.Main in your main function, and flag parsing is taken care of.
    32  // If a more complicated ordering is required, you can call cmdline.Parse and
    33  // then handle any special initialization.
    34  //
    35  // The problem is that flags registered on the root command must be merged
    36  // together with the global flags for the root command to be parsed.  If
    37  // flag.Parse is called before cmdline.Main or cmdline.Parse, it will fail if
    38  // any root command flags are specified on the command line.
    39  package cmdline
    40  
    41  import (
    42  	"flag"
    43  	"fmt"
    44  	"io"
    45  	"io/ioutil"
    46  	"os"
    47  	"os/exec"
    48  	"path/filepath"
    49  	"reflect"
    50  	"sort"
    51  	"strings"
    52  	"syscall"
    53  
    54  	"github.com/btwiuse/jiri/envvar"
    55  	_ "github.com/btwiuse/jiri/metadata" // for the -metadata flag
    56  	"github.com/btwiuse/jiri/timing"
    57  )
    58  
    59  // Command represents a single command in a command-line program.  A program
    60  // with subcommands is represented as a root Command with children representing
    61  // each subcommand.  The command graph must be a tree; each command may either
    62  // have no parent (the root) or exactly one parent, and cycles are not allowed.
    63  type Command struct {
    64  	Name     string // Name of the command.
    65  	Short    string // Short description, shown in help called on parent.
    66  	Long     string // Long description, shown in help called on itself.
    67  	ArgsName string // Name of the args, shown in usage line.
    68  	ArgsLong string // Long description of the args, shown in help.
    69  
    70  	// Flags defined for this command.  When a flag F is defined on a command C,
    71  	// we allow F to be specified on the command line immediately after C, or
    72  	// after any descendant of C. This FlagSet is only used to specify the
    73  	// flags and their associated value variables, it is never parsed and hence
    74  	// methods on FlagSet that are generally used after parsing cannot be
    75  	// used on Flags. ParsedFlags should be used instead.
    76  	Flags flag.FlagSet
    77  	// ParsedFlags contains the FlagSet created by the Command
    78  	// implementation and that has had its Parse method called. It
    79  	// should be used instead of the Flags field for handling methods
    80  	// that assume Parse has been called (e.g. Parsed, Visit,
    81  	// NArgs etc).
    82  	ParsedFlags *flag.FlagSet
    83  	// DontPropagateFlags indicates whether to prevent the flags defined on this
    84  	// command and the ancestor commands from being propagated to the descendant
    85  	// commands.
    86  	DontPropagateFlags bool
    87  	// DontInheritFlags indicates whether to stop inheriting the flags from the
    88  	// ancestor commands. The flags for the ancestor commands will not be
    89  	// propagated to the child commands as well.
    90  	DontInheritFlags bool
    91  
    92  	// Children of the command.
    93  	Children []*Command
    94  
    95  	// LookPath indicates whether to look for external subcommands in the
    96  	// directories specified by the PATH environment variable.  The compiled-in
    97  	// children always take precedence; the check for external children only
    98  	// occurs if none of the compiled-in children match.
    99  	//
   100  	// All global flags and flags set on ancestor commands are passed through to
   101  	// the external child.
   102  	LookPath bool
   103  
   104  	// Runner that runs the command.
   105  	// Use RunnerFunc to adapt regular functions into Runners.
   106  	//
   107  	// At least one of Children or Runner must be specified.  If both are
   108  	// specified, ArgsName and ArgsLong must be empty, meaning the Runner doesn't
   109  	// take any args.  Otherwise there's a possible conflict between child names
   110  	// and the runner args, and an error is returned from Parse.
   111  	Runner Runner
   112  
   113  	// Topics that provide additional info via the default help command.
   114  	Topics []Topic
   115  }
   116  
   117  // Runner is the interface for running commands.  Return ErrExitCode to indicate
   118  // the command should exit with a specific exit code.
   119  type Runner interface {
   120  	Run(env *Env, args []string) error
   121  }
   122  
   123  // RunnerFunc is an adapter that turns regular functions into Runners.
   124  type RunnerFunc func(*Env, []string) error
   125  
   126  // Run implements the Runner interface method by calling f(env, args).
   127  func (f RunnerFunc) Run(env *Env, args []string) error {
   128  	return f(env, args)
   129  }
   130  
   131  // Topic represents a help topic that is accessed via the help command.
   132  type Topic struct {
   133  	Name  string // Name of the topic.
   134  	Short string // Short description, shown in help for the command.
   135  	Long  string // Long description, shown in help for this topic.
   136  }
   137  
   138  // Main implements the main function for the command tree rooted at root.
   139  //
   140  // It initializes a new environment from the underlying operating system, parses
   141  // os.Args[1:] against the root command, and runs the resulting runner.  Calls
   142  // os.Exit with an exit code that is 0 for success, or non-zero for errors.
   143  //
   144  // Most main packages should be implemented as follows:
   145  //
   146  //   var root := &cmdline.Command{...}
   147  //
   148  //   func main() {
   149  //     cmdline.Main(root)
   150  //   }
   151  func Main(root *Command) {
   152  	env := EnvFromOS()
   153  	if env.Timer != nil && len(env.Timer.Intervals) > 0 {
   154  		env.Timer.Intervals[0].Name = pathName(env.prefix(), []*Command{root})
   155  	}
   156  	err := ParseAndRun(root, env, os.Args[1:])
   157  	code := ExitCode(err, env.Stderr)
   158  	if *flagTime && env.Timer != nil {
   159  		env.Timer.Finish()
   160  		p := timing.IntervalPrinter{Zero: env.Timer.Zero}
   161  		if err := p.Print(env.Stderr, env.Timer.Intervals, env.Timer.Now()); err != nil {
   162  			code2 := ExitCode(err, env.Stderr)
   163  			if code == 0 {
   164  				code = code2
   165  			}
   166  		}
   167  	}
   168  	os.Exit(code)
   169  }
   170  
   171  var flagTime = flag.Bool("time", false, "Dump timing information to stderr before exiting the program.")
   172  
   173  // Parse parses args against the command tree rooted at root down to a leaf
   174  // command.  A single path through the command tree is traversed, based on the
   175  // sub-commands specified in args.  Global and command-specific flags are parsed
   176  // as the tree is traversed.
   177  //
   178  // On success returns the runner corresponding to the leaf command, along with
   179  // the args to pass to the runner.  In addition the env.Usage function is set to
   180  // produce a usage message corresponding to the leaf command.
   181  //
   182  // Most main packages should just call Main.  Parse should only be used if
   183  // special processing is required after parsing the args, and before the runner
   184  // is run.  An example:
   185  //
   186  //   var root := &cmdline.Command{...}
   187  //
   188  //   func main() {
   189  //     env := cmdline.EnvFromOS()
   190  //     os.Exit(cmdline.ExitCode(parseAndRun(env), env.Stderr))
   191  //   }
   192  //
   193  //   func parseAndRun(env *cmdline.Env) error {
   194  //     runner, args, err := cmdline.Parse(env, root, os.Args[1:])
   195  //     if err != nil {
   196  //       return err
   197  //     }
   198  //     // ... perform initialization that might parse flags ...
   199  //     return runner.Run(env, args)
   200  //   }
   201  //
   202  // Parse merges root flags into flag.CommandLine and sets ContinueOnError, so
   203  // that subsequent calls to flag.Parsed return true.
   204  func Parse(root *Command, env *Env, args []string) (Runner, []string, error) {
   205  	env.TimerPush("cmdline parse")
   206  	defer env.TimerPop()
   207  	if globalFlags == nil {
   208  		// Initialize our global flags to a cleaned copy.  We don't want the merging
   209  		// in parseFlags to contaminate the global flags, even if Parse is called
   210  		// multiple times, so we keep a single package-level copy.
   211  		cleanFlags(flag.CommandLine)
   212  		globalFlags = copyFlags(flag.CommandLine)
   213  	}
   214  	// Set env.Usage to the usage of the root command, in case the parse fails.
   215  	path := []*Command{root}
   216  
   217  	env.Usage = makeHelpRunner(path, env).usageFunc
   218  	cleanTree(root)
   219  	if err := checkTreeInvariants(path, env); err != nil {
   220  		return nil, nil, err
   221  	}
   222  	runner, args, err := root.parse(nil, env, args, make(map[string]string))
   223  	if err != nil {
   224  		return nil, nil, err
   225  	}
   226  	// Clear envvars that start with "CMDLINE_" when returning a user-specified
   227  	// runner, to avoid polluting the environment.  In particular CMDLINE_PREFIX
   228  	// and CMDLINE_FIRST_CALL are only meant to be passed to external children,
   229  	// and shouldn't be propagated through the user's runner.
   230  	switch runner.(type) {
   231  	case helpRunner, binaryRunner:
   232  		// The help and binary runners need the envvars to be set.
   233  	default:
   234  		for key, _ := range env.Vars {
   235  			if strings.HasPrefix(key, "CMDLINE_") {
   236  				delete(env.Vars, key)
   237  				if err := os.Unsetenv(key); err != nil {
   238  					return nil, nil, err
   239  				}
   240  			}
   241  		}
   242  	}
   243  	return runner, args, nil
   244  }
   245  
   246  var globalFlags *flag.FlagSet
   247  
   248  // ParseAndRun is a convenience that calls Parse, and then calls Run on the
   249  // returned runner with the given env and parsed args.
   250  func ParseAndRun(root *Command, env *Env, args []string) error {
   251  	runner, args, err := Parse(root, env, args)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	env.TimerPush("cmdline run")
   256  	defer env.TimerPop()
   257  	return runner.Run(env, args)
   258  }
   259  
   260  func trimSpace(s *string) { *s = strings.TrimSpace(*s) }
   261  
   262  func cleanTree(cmd *Command) {
   263  	trimSpace(&cmd.Name)
   264  	trimSpace(&cmd.Short)
   265  	trimSpace(&cmd.Long)
   266  	trimSpace(&cmd.ArgsName)
   267  	trimSpace(&cmd.ArgsLong)
   268  	for tx := range cmd.Topics {
   269  		trimSpace(&cmd.Topics[tx].Name)
   270  		trimSpace(&cmd.Topics[tx].Short)
   271  		trimSpace(&cmd.Topics[tx].Long)
   272  	}
   273  	cleanFlags(&cmd.Flags)
   274  	for _, child := range cmd.Children {
   275  		cleanTree(child)
   276  	}
   277  }
   278  
   279  func cleanFlags(flags *flag.FlagSet) {
   280  	flags.VisitAll(func(f *flag.Flag) {
   281  		trimSpace(&f.Usage)
   282  	})
   283  }
   284  
   285  func checkTreeInvariants(path []*Command, env *Env) error {
   286  	cmd, cmdPath := path[len(path)-1], pathName(env.prefix(), path)
   287  	// Check that the root name is non-empty.
   288  	if cmdPath == "" {
   289  		return fmt.Errorf(`CODE INVARIANT BROKEN; FIX YOUR CODE
   290  
   291  Root command name cannot be empty.`)
   292  	}
   293  	// Check that the children and topic names are non-empty and unique.
   294  	seen := make(map[string]bool)
   295  	checkName := func(name string) error {
   296  		if name == "" {
   297  			return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE
   298  
   299  Command and topic names cannot be empty.`, cmdPath)
   300  		}
   301  		if seen[name] {
   302  			return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE
   303  
   304  Each command must have unique children and topic names.
   305  Saw %q multiple times.`, cmdPath, name)
   306  		}
   307  		seen[name] = true
   308  		return nil
   309  	}
   310  	for _, child := range cmd.Children {
   311  		if err := checkName(child.Name); err != nil {
   312  			return err
   313  		}
   314  	}
   315  	for _, topic := range cmd.Topics {
   316  		if err := checkName(topic.Name); err != nil {
   317  			return err
   318  		}
   319  	}
   320  	// Check that our Children / Runner invariant is satisfied.  At least one must
   321  	// be specified, and if both are specified then ArgsName and ArgsLong must be
   322  	// empty, meaning the Runner doesn't take any args.
   323  	switch hasC, hasR := len(cmd.Children) > 0, cmd.Runner != nil; {
   324  	case !hasC && !hasR:
   325  		return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE
   326  
   327  At least one of Children or Runner must be specified.`, cmdPath)
   328  	case hasC && hasR && (cmd.ArgsName != "" || cmd.ArgsLong != ""):
   329  		return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE
   330  
   331  Since both Children and Runner are specified, the Runner cannot take args.
   332  Otherwise a conflict between child names and runner args is possible.`, cmdPath)
   333  	}
   334  	// Check recursively for all children
   335  	for _, child := range cmd.Children {
   336  		if err := checkTreeInvariants(append(path, child), env); err != nil {
   337  			return err
   338  		}
   339  	}
   340  	return nil
   341  }
   342  
   343  func pathName(prefix string, path []*Command) string {
   344  	name := prefix
   345  	for _, cmd := range path {
   346  		if name != "" {
   347  			name += " "
   348  		}
   349  		name += cmd.Name
   350  	}
   351  	return name
   352  }
   353  
   354  func (cmd *Command) parse(path []*Command, env *Env, args []string, setFlags map[string]string) (Runner, []string, error) {
   355  	path = append(path, cmd)
   356  	cmdPath := pathName(env.prefix(), path)
   357  	runHelp := makeHelpRunner(path, env)
   358  	env.Usage = runHelp.usageFunc
   359  	// Parse flags and retrieve the args remaining after the parse, as well as the
   360  	// flags that were set.
   361  	args, setF, err := parseFlags(path, env, args)
   362  	switch {
   363  	case err == flag.ErrHelp:
   364  		return runHelp, nil, nil
   365  	case err != nil:
   366  		return nil, nil, env.UsageErrorf("%s: %v", cmdPath, err)
   367  	}
   368  	for key, val := range setF {
   369  		setFlags[key] = val
   370  	}
   371  	// First handle the no-args case.
   372  	if len(args) == 0 {
   373  		if cmd.Runner != nil {
   374  			return cmd.Runner, nil, nil
   375  		}
   376  		return nil, nil, env.UsageErrorf("%s: no command specified", cmdPath)
   377  	}
   378  	// INVARIANT: len(args) > 0
   379  	// Look for matching children.
   380  	subName, subArgs := args[0], args[1:]
   381  	if len(cmd.Children) > 0 {
   382  		for _, child := range cmd.Children {
   383  			if child.Name == subName {
   384  				if env.CommandName != "" {
   385  					env.CommandName = env.CommandName + "->" + subName
   386  				} else {
   387  					env.CommandName = subName
   388  				}
   389  				return child.parse(path, env, subArgs, setFlags)
   390  			}
   391  		}
   392  		// Every non-leaf command gets a default help command.
   393  		if helpName == subName {
   394  			env.CommandName = subName
   395  			return runHelp.newCommand().parse(path, env, subArgs, setFlags)
   396  		}
   397  	}
   398  	if cmd.LookPath {
   399  		// Look for a matching executable in PATH.
   400  		if subCmd, _ := env.LookPath(cmd.Name + "-" + subName); subCmd != "" {
   401  			extArgs := append(flagsAsArgs(setFlags), subArgs...)
   402  			return binaryRunner{subCmd, cmdPath}, extArgs, nil
   403  		}
   404  	}
   405  	// No matching subcommands, check various error cases.
   406  	switch {
   407  	case cmd.Runner == nil:
   408  		return nil, nil, env.UsageErrorf("%s: unknown command %q", cmdPath, subName)
   409  	case cmd.ArgsName == "":
   410  		if len(cmd.Children) > 0 {
   411  			return nil, nil, env.UsageErrorf("%s: unknown command %q", cmdPath, subName)
   412  		}
   413  		return nil, nil, env.UsageErrorf("%s: doesn't take arguments", cmdPath)
   414  	case reflect.DeepEqual(args, []string{helpName, "..."}):
   415  		return nil, nil, env.UsageErrorf("%s: unsupported help invocation", cmdPath)
   416  	}
   417  	// INVARIANT:
   418  	// cmd.Runner != nil && len(args) > 0 &&
   419  	// cmd.ArgsName != "" && args != []string{"help", "..."}
   420  	return cmd.Runner, args, nil
   421  }
   422  
   423  // parseFlags parses the flags from args for the command with the given path and
   424  // env.  Returns the remaining non-flag args and the flags that were set.
   425  func parseFlags(path []*Command, env *Env, args []string) ([]string, map[string]string, error) {
   426  	cmd, isRoot := path[len(path)-1], len(path) == 1
   427  	// Parse the merged command-specific and global flags.
   428  	var flags *flag.FlagSet
   429  	if isRoot {
   430  		// The root command is special, due to the pitfall described above in the
   431  		// package doc.  Merge into flag.CommandLine and use that for parsing.  This
   432  		// ensures that subsequent calls to flag.Parsed will return true, so the
   433  		// user can check whether flags have already been parsed.  Global flags take
   434  		// precedence over command flags for the root command.
   435  		flags = flag.CommandLine
   436  		mergeFlags(flags, &cmd.Flags)
   437  	} else {
   438  		// Command flags take precedence over global flags for non-root commands.
   439  		flags = pathFlags(path)
   440  		mergeFlags(flags, globalFlags)
   441  	}
   442  	// Silence the many different ways flags.Parse can produce ugly output; we
   443  	// just want it to return any errors and handle the output ourselves.
   444  	//   1) Set flag.ContinueOnError so that Parse() doesn't exit or panic.
   445  	//   2) Discard all output (can't be nil, that means stderr).
   446  	//   3) Set an empty Usage (can't be nil, that means use the default).
   447  	flags.Init(cmd.Name, flag.ContinueOnError)
   448  	flags.SetOutput(ioutil.Discard)
   449  	flags.Usage = func() {}
   450  	if isRoot {
   451  		// If this is the root command, we must remember to undo the above changes
   452  		// on flag.CommandLine after the parse.  We don't know the original settings
   453  		// of these values, so we just blindly set back to the default values.
   454  		defer func() {
   455  			flags.Init(cmd.Name, flag.ExitOnError)
   456  			flags.SetOutput(nil)
   457  			flags.Usage = func() { env.Usage(env, env.Stderr) }
   458  		}()
   459  	}
   460  	if err := flags.Parse(args); err != nil {
   461  		return nil, nil, err
   462  	}
   463  	cmd.ParsedFlags = flags
   464  	env.CommandFlags = make(map[string]string)
   465  	flags.Visit(func(f *flag.Flag) {
   466  		val := f.Value.String()
   467  		env.CommandFlags[f.Name] = val
   468  	})
   469  	return flags.Args(), extractSetFlags(flags), nil
   470  }
   471  
   472  func mergeFlags(dst, src *flag.FlagSet) {
   473  	src.VisitAll(func(f *flag.Flag) {
   474  		// If there is a collision in flag names, the existing flag in dst wins.
   475  		// Note that flag.Var will panic if it sees a collision.
   476  		if dst.Lookup(f.Name) == nil {
   477  			dst.Var(f.Value, f.Name, f.Usage)
   478  			dst.Lookup(f.Name).DefValue = f.DefValue
   479  		}
   480  	})
   481  }
   482  
   483  func copyFlags(flags *flag.FlagSet) *flag.FlagSet {
   484  	cp := new(flag.FlagSet)
   485  	mergeFlags(cp, flags)
   486  	return cp
   487  }
   488  
   489  // pathFlags returns the flags that are allowed for the last command in the
   490  // path.  Flags defined on ancestors are also allowed, except on "help".
   491  func pathFlags(path []*Command) *flag.FlagSet {
   492  	cmd := path[len(path)-1]
   493  	flags := copyFlags(&cmd.Flags)
   494  	if cmd.Name != helpName && !cmd.DontInheritFlags {
   495  		// Walk backwards to merge flags up to the root command.  If this takes too
   496  		// long, we could consider memoizing previous results.
   497  		for p := len(path) - 2; p >= 0; p-- {
   498  			if path[p].DontPropagateFlags {
   499  				break
   500  			}
   501  			mergeFlags(flags, &path[p].Flags)
   502  			if path[p].DontInheritFlags {
   503  				break
   504  			}
   505  		}
   506  	}
   507  	return flags
   508  }
   509  
   510  func extractSetFlags(flags *flag.FlagSet) map[string]string {
   511  	// Use FlagSet.Visit rather than VisitAll to restrict to flags that are set.
   512  	setFlags := make(map[string]string)
   513  	flags.Visit(func(f *flag.Flag) {
   514  		setFlags[f.Name] = f.Value.String()
   515  	})
   516  	return setFlags
   517  }
   518  
   519  func flagsAsArgs(x map[string]string) []string {
   520  	var args []string
   521  	for key, val := range x {
   522  		args = append(args, "-"+key+"="+val)
   523  	}
   524  	sort.Strings(args)
   525  	return args
   526  }
   527  
   528  // subNames returns the sub names of c which should be ignored when using look
   529  // path to find external binaries.
   530  func (c *Command) subNames(prefix string) map[string]bool {
   531  	m := map[string]bool{prefix + "help": true}
   532  	for _, child := range c.Children {
   533  		m[prefix+child.Name] = true
   534  	}
   535  	return m
   536  }
   537  
   538  // ErrExitCode may be returned by Runner.Run to cause the program to exit with a
   539  // specific error code.
   540  type ErrExitCode int
   541  
   542  // Error implements the error interface method.
   543  func (x ErrExitCode) Error() string {
   544  	return fmt.Sprintf("exit code %d", x)
   545  }
   546  
   547  // ErrUsage indicates an error in command usage; e.g. unknown flags, subcommands
   548  // or args.  It corresponds to exit code 2.
   549  const ErrUsage = ErrExitCode(2)
   550  
   551  // ExitCode returns the exit code corresponding to err.
   552  //   0:    if err == nil
   553  //   code: if err is ErrExitCode(code)
   554  //   1:    all other errors
   555  // Writes the error message for "all other errors" to w, if w is non-nil.
   556  func ExitCode(err error, w io.Writer) int {
   557  	if err == nil {
   558  		return 0
   559  	}
   560  	if code, ok := err.(ErrExitCode); ok {
   561  		return int(code)
   562  	}
   563  	if w != nil {
   564  		// We don't print "ERROR: exit code N" above to avoid cluttering the output.
   565  		fmt.Fprintf(w, "ERROR: %v\n", err)
   566  	}
   567  	return 1
   568  }
   569  
   570  type binaryRunner struct {
   571  	subCmd  string
   572  	cmdPath string
   573  }
   574  
   575  func (b binaryRunner) Run(env *Env, args []string) error {
   576  	env.TimerPush("run " + filepath.Base(b.subCmd))
   577  	defer env.TimerPop()
   578  	vars := envvar.CopyMap(env.Vars)
   579  	vars["CMDLINE_PREFIX"] = b.cmdPath
   580  	cmd := exec.Command(b.subCmd, args...)
   581  	cmd.Stdin = env.Stdin
   582  	cmd.Stdout = env.Stdout
   583  	cmd.Stderr = env.Stderr
   584  	cmd.Env = envvar.MapToSlice(vars)
   585  	err := cmd.Run()
   586  	// Make sure we return the exit code from the binary, if it exited.
   587  	if exitError, ok := err.(*exec.ExitError); ok {
   588  		if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
   589  			return ErrExitCode(status.ExitStatus())
   590  		}
   591  	}
   592  	return err
   593  }