github.com/dooferlad/cmd@v0.0.0-20150716022859-3edef806220b/cmd.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENSE file for details.
     3  
     4  package cmd
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"os/signal"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"launchpad.net/gnuflag"
    18  )
    19  
    20  type RcPassthroughError struct {
    21  	Code int
    22  }
    23  
    24  func (e *RcPassthroughError) Error() string {
    25  	return fmt.Sprintf("subprocess encountered error code %v", e.Code)
    26  }
    27  
    28  func IsRcPassthroughError(err error) bool {
    29  	_, ok := err.(*RcPassthroughError)
    30  	return ok
    31  }
    32  
    33  // NewRcPassthroughError creates an error that will have the code used at the
    34  // return code from the cmd.Main function rather than the default of 1 if
    35  // there is an error.
    36  func NewRcPassthroughError(code int) error {
    37  	return &RcPassthroughError{code}
    38  }
    39  
    40  // ErrSilent can be returned from Run to signal that Main should exit with
    41  // code 1 without producing error output.
    42  var ErrSilent = errors.New("cmd: error out silently")
    43  
    44  // Command is implemented by types that interpret command-line arguments.
    45  type Command interface {
    46  	// IsSuperCommand returns true if the command is a super command.
    47  	IsSuperCommand() bool
    48  
    49  	// Info returns information about the Command.
    50  	Info() *Info
    51  
    52  	// SetFlags adds command specific flags to the flag set.
    53  	SetFlags(f *gnuflag.FlagSet)
    54  
    55  	// Init initializes the Command before running.
    56  	Init(args []string) error
    57  
    58  	// Run will execute the Command as directed by the options and positional
    59  	// arguments passed to Init.
    60  	Run(ctx *Context) error
    61  
    62  	// AllowInterspersedFlags returns whether the command allows flag
    63  	// arguments to be interspersed with non-flag arguments.
    64  	AllowInterspersedFlags() bool
    65  }
    66  
    67  // CommandBase provides the default implementation for SetFlags, Init, and Help.
    68  type CommandBase struct{}
    69  
    70  // IsSuperCommand implements Command.IsSuperCommand
    71  func (c *CommandBase) IsSuperCommand() bool {
    72  	return false
    73  }
    74  
    75  // SetFlags does nothing in the simplest case.
    76  func (c *CommandBase) SetFlags(f *gnuflag.FlagSet) {}
    77  
    78  // Init in the simplest case makes sure there are no args.
    79  func (c *CommandBase) Init(args []string) error {
    80  	return CheckEmpty(args)
    81  }
    82  
    83  // AllowInterspersedFlags returns true by default. Some subcommands
    84  // may want to override this.
    85  func (c *CommandBase) AllowInterspersedFlags() bool {
    86  	return true
    87  }
    88  
    89  // Context represents the run context of a Command. Command implementations
    90  // should interpret file names relative to Dir (see AbsPath below), and print
    91  // output and errors to Stdout and Stderr respectively.
    92  type Context struct {
    93  	Dir     string
    94  	Env     map[string]string
    95  	Stdin   io.Reader
    96  	Stdout  io.Writer
    97  	Stderr  io.Writer
    98  	quiet   bool
    99  	verbose bool
   100  }
   101  
   102  func (ctx *Context) write(format string, params ...interface{}) {
   103  	output := fmt.Sprintf(format, params...)
   104  	if !strings.HasSuffix(output, "\n") {
   105  		output = output + "\n"
   106  	}
   107  	fmt.Fprint(ctx.Stderr, output)
   108  }
   109  
   110  // Infof will write the formatted string to Stderr if quiet is false, but if
   111  // quiet is true the message is logged.
   112  func (ctx *Context) Infof(format string, params ...interface{}) {
   113  	if ctx.quiet {
   114  		logger.Infof(format, params...)
   115  	} else {
   116  		ctx.write(format, params...)
   117  	}
   118  }
   119  
   120  // Verbosef will write the formatted string to Stderr if the verbose is true,
   121  // and to the logger if not.
   122  func (ctx *Context) Verbosef(format string, params ...interface{}) {
   123  	if ctx.verbose {
   124  		ctx.write(format, params...)
   125  	} else {
   126  		logger.Infof(format, params...)
   127  	}
   128  }
   129  
   130  // Getenv looks up an environment variable in the context. It mirrors
   131  // os.Getenv. An empty string is returned if the key is not set.
   132  func (ctx *Context) Getenv(key string) string {
   133  	value, _ := ctx.Env[key]
   134  	return value
   135  }
   136  
   137  // Setenv sets an environment variable in the context. It mirrors os.Setenv.
   138  func (ctx *Context) Setenv(key, value string) error {
   139  	if ctx.Env == nil {
   140  		ctx.Env = make(map[string]string)
   141  	}
   142  	ctx.Env[key] = value
   143  	return nil
   144  }
   145  
   146  // AbsPath returns an absolute representation of path, with relative paths
   147  // interpreted as relative to ctx.Dir.
   148  func (ctx *Context) AbsPath(path string) string {
   149  	if filepath.IsAbs(path) {
   150  		return path
   151  	}
   152  	return filepath.Join(ctx.Dir, path)
   153  }
   154  
   155  // GetStdin satisfies environs.BootstrapContext
   156  func (ctx *Context) GetStdin() io.Reader {
   157  	return ctx.Stdin
   158  }
   159  
   160  // GetStdout satisfies environs.BootstrapContext
   161  func (ctx *Context) GetStdout() io.Writer {
   162  	return ctx.Stdout
   163  }
   164  
   165  // GetStderr satisfies environs.BootstrapContext
   166  func (ctx *Context) GetStderr() io.Writer {
   167  	return ctx.Stderr
   168  }
   169  
   170  // InterruptNotify satisfies environs.BootstrapContext
   171  func (ctx *Context) InterruptNotify(c chan<- os.Signal) {
   172  	signal.Notify(c, os.Interrupt)
   173  }
   174  
   175  // StopInterruptNotify satisfies environs.BootstrapContext
   176  func (ctx *Context) StopInterruptNotify(c chan<- os.Signal) {
   177  	signal.Stop(c)
   178  }
   179  
   180  // Info holds some of the usage documentation of a Command.
   181  type Info struct {
   182  	// Name is the Command's name.
   183  	Name string
   184  
   185  	// Args describes the command's expected positional arguments.
   186  	Args string
   187  
   188  	// Purpose is a short explanation of the Command's purpose.
   189  	Purpose string
   190  
   191  	// Doc is the long documentation for the Command.
   192  	Doc string
   193  
   194  	// Aliases are other names for the Command.
   195  	Aliases []string
   196  }
   197  
   198  // Help renders i's content, along with documentation for any
   199  // flags defined in f. It calls f.SetOutput(ioutil.Discard).
   200  func (i *Info) Help(f *gnuflag.FlagSet) []byte {
   201  	buf := &bytes.Buffer{}
   202  	fmt.Fprintf(buf, "usage: %s", i.Name)
   203  	hasOptions := false
   204  	f.VisitAll(func(f *gnuflag.Flag) { hasOptions = true })
   205  	if hasOptions {
   206  		fmt.Fprintf(buf, " [options]")
   207  	}
   208  	if i.Args != "" {
   209  		fmt.Fprintf(buf, " %s", i.Args)
   210  	}
   211  	fmt.Fprintf(buf, "\n")
   212  	if i.Purpose != "" {
   213  		fmt.Fprintf(buf, "purpose: %s\n", i.Purpose)
   214  	}
   215  	if hasOptions {
   216  		fmt.Fprintf(buf, "\noptions:\n")
   217  		f.SetOutput(buf)
   218  		f.PrintDefaults()
   219  	}
   220  	f.SetOutput(ioutil.Discard)
   221  	if i.Doc != "" {
   222  		fmt.Fprintf(buf, "\n%s\n", strings.TrimSpace(i.Doc))
   223  	}
   224  	if len(i.Aliases) > 0 {
   225  		fmt.Fprintf(buf, "\naliases: %s\n", strings.Join(i.Aliases, ", "))
   226  	}
   227  	return buf.Bytes()
   228  }
   229  
   230  // Errors from commands can be ErrSilent (don't print an error message),
   231  // ErrHelp (show the help) or some other error related to needed flags
   232  // missing, or needed positional args missing, in which case we should
   233  // print the error and return a non-zero return code.
   234  func handleCommandError(c Command, ctx *Context, err error, f *gnuflag.FlagSet) (rc int, done bool) {
   235  	switch err {
   236  	case nil:
   237  		return 0, false
   238  	case gnuflag.ErrHelp:
   239  		ctx.Stdout.Write(c.Info().Help(f))
   240  		return 0, true
   241  	case ErrSilent:
   242  		return 2, true
   243  	default:
   244  		fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
   245  		return 2, true
   246  	}
   247  }
   248  
   249  // Main runs the given Command in the supplied Context with the given
   250  // arguments, which should not include the command name. It returns a code
   251  // suitable for passing to os.Exit.
   252  func Main(c Command, ctx *Context, args []string) int {
   253  	f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError)
   254  	f.SetOutput(ioutil.Discard)
   255  	c.SetFlags(f)
   256  	if rc, done := handleCommandError(c, ctx, f.Parse(c.AllowInterspersedFlags(), args), f); done {
   257  		return rc
   258  	}
   259  	// Since SuperCommands can also return gnuflag.ErrHelp errors, we need to
   260  	// handle both those types of errors as well as "real" errors.
   261  	if rc, done := handleCommandError(c, ctx, c.Init(f.Args()), f); done {
   262  		return rc
   263  	}
   264  	if err := c.Run(ctx); err != nil {
   265  		if IsRcPassthroughError(err) {
   266  			return err.(*RcPassthroughError).Code
   267  		}
   268  		if err != ErrSilent {
   269  			fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
   270  		}
   271  		return 1
   272  	}
   273  	return 0
   274  }
   275  
   276  // DefaultContext returns a Context suitable for use in non-hosted situations.
   277  func DefaultContext() (*Context, error) {
   278  	dir, err := os.Getwd()
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	abs, err := filepath.Abs(dir)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	return &Context{
   287  		Dir:    abs,
   288  		Stdin:  os.Stdin,
   289  		Stdout: os.Stdout,
   290  		Stderr: os.Stderr,
   291  	}, nil
   292  }
   293  
   294  // CheckEmpty is a utility function that returns an error if args is not empty.
   295  func CheckEmpty(args []string) error {
   296  	if len(args) != 0 {
   297  		return fmt.Errorf("unrecognized args: %q", args)
   298  	}
   299  	return nil
   300  }
   301  
   302  // ZeroOrOneArgs checks to see that there are zero or one args, and returns
   303  // the value of the arg if provided, or the empty string if not.
   304  func ZeroOrOneArgs(args []string) (string, error) {
   305  	var result string
   306  	if len(args) > 0 {
   307  		result, args = args[0], args[1:]
   308  	}
   309  	if err := CheckEmpty(args); err != nil {
   310  		return "", err
   311  	}
   312  	return result, nil
   313  }