github.com/kat-co/cmd@v0.0.0-20140616103059-5da365f9d57e/cmd.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE 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  	Stdin   io.Reader
    95  	Stdout  io.Writer
    96  	Stderr  io.Writer
    97  	quiet   bool
    98  	verbose bool
    99  }
   100  
   101  func (ctx *Context) write(format string, params ...interface{}) {
   102  	output := fmt.Sprintf(format, params...)
   103  	if !strings.HasSuffix(output, "\n") {
   104  		output = output + "\n"
   105  	}
   106  	fmt.Fprint(ctx.Stderr, output)
   107  }
   108  
   109  // Infof will write the formatted string to Stderr if quiet is false, but if
   110  // quiet is true the message is logged.
   111  func (ctx *Context) Infof(format string, params ...interface{}) {
   112  	if ctx.quiet {
   113  		logger.Infof(format, params...)
   114  	} else {
   115  		ctx.write(format, params...)
   116  	}
   117  }
   118  
   119  // Verbosef will write the formatted string to Stderr if the verbose is true,
   120  // and to the logger if not.
   121  func (ctx *Context) Verbosef(format string, params ...interface{}) {
   122  	if ctx.verbose {
   123  		ctx.write(format, params...)
   124  	} else {
   125  		logger.Infof(format, params...)
   126  	}
   127  }
   128  
   129  // AbsPath returns an absolute representation of path, with relative paths
   130  // interpreted as relative to ctx.Dir.
   131  func (ctx *Context) AbsPath(path string) string {
   132  	if filepath.IsAbs(path) {
   133  		return path
   134  	}
   135  	return filepath.Join(ctx.Dir, path)
   136  }
   137  
   138  // GetStdin satisfies environs.BootstrapContext
   139  func (ctx *Context) GetStdin() io.Reader {
   140  	return ctx.Stdin
   141  }
   142  
   143  // GetStdout satisfies environs.BootstrapContext
   144  func (ctx *Context) GetStdout() io.Writer {
   145  	return ctx.Stdout
   146  }
   147  
   148  // GetStderr satisfies environs.BootstrapContext
   149  func (ctx *Context) GetStderr() io.Writer {
   150  	return ctx.Stderr
   151  }
   152  
   153  // InterruptNotify satisfies environs.BootstrapContext
   154  func (ctx *Context) InterruptNotify(c chan<- os.Signal) {
   155  	signal.Notify(c, os.Interrupt)
   156  }
   157  
   158  // StopInterruptNotify satisfies environs.BootstrapContext
   159  func (ctx *Context) StopInterruptNotify(c chan<- os.Signal) {
   160  	signal.Stop(c)
   161  }
   162  
   163  // Info holds some of the usage documentation of a Command.
   164  type Info struct {
   165  	// Name is the Command's name.
   166  	Name string
   167  
   168  	// Args describes the command's expected positional arguments.
   169  	Args string
   170  
   171  	// Purpose is a short explanation of the Command's purpose.
   172  	Purpose string
   173  
   174  	// Doc is the long documentation for the Command.
   175  	Doc string
   176  
   177  	// Aliases are other names for the Command.
   178  	Aliases []string
   179  }
   180  
   181  // Help renders i's content, along with documentation for any
   182  // flags defined in f. It calls f.SetOutput(ioutil.Discard).
   183  func (i *Info) Help(f *gnuflag.FlagSet) []byte {
   184  	buf := &bytes.Buffer{}
   185  	fmt.Fprintf(buf, "usage: %s", i.Name)
   186  	hasOptions := false
   187  	f.VisitAll(func(f *gnuflag.Flag) { hasOptions = true })
   188  	if hasOptions {
   189  		fmt.Fprintf(buf, " [options]")
   190  	}
   191  	if i.Args != "" {
   192  		fmt.Fprintf(buf, " %s", i.Args)
   193  	}
   194  	fmt.Fprintf(buf, "\n")
   195  	if i.Purpose != "" {
   196  		fmt.Fprintf(buf, "purpose: %s\n", i.Purpose)
   197  	}
   198  	if hasOptions {
   199  		fmt.Fprintf(buf, "\noptions:\n")
   200  		f.SetOutput(buf)
   201  		f.PrintDefaults()
   202  	}
   203  	f.SetOutput(ioutil.Discard)
   204  	if i.Doc != "" {
   205  		fmt.Fprintf(buf, "\n%s\n", strings.TrimSpace(i.Doc))
   206  	}
   207  	if len(i.Aliases) > 0 {
   208  		fmt.Fprintf(buf, "\naliases: %s\n", strings.Join(i.Aliases, ", "))
   209  	}
   210  	return buf.Bytes()
   211  }
   212  
   213  // Errors from commands can be ErrSilent (don't print an error message),
   214  // ErrHelp (show the help) or some other error related to needed flags
   215  // missing, or needed positional args missing, in which case we should
   216  // print the error and return a non-zero return code.
   217  func handleCommandError(c Command, ctx *Context, err error, f *gnuflag.FlagSet) (rc int, done bool) {
   218  	switch err {
   219  	case nil:
   220  		return 0, false
   221  	case gnuflag.ErrHelp:
   222  		ctx.Stdout.Write(c.Info().Help(f))
   223  		return 0, true
   224  	case ErrSilent:
   225  		return 2, true
   226  	default:
   227  		fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
   228  		return 2, true
   229  	}
   230  }
   231  
   232  // Main runs the given Command in the supplied Context with the given
   233  // arguments, which should not include the command name. It returns a code
   234  // suitable for passing to os.Exit.
   235  func Main(c Command, ctx *Context, args []string) int {
   236  	f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError)
   237  	f.SetOutput(ioutil.Discard)
   238  	c.SetFlags(f)
   239  	if rc, done := handleCommandError(c, ctx, f.Parse(c.AllowInterspersedFlags(), args), f); done {
   240  		return rc
   241  	}
   242  	// Since SuperCommands can also return gnuflag.ErrHelp errors, we need to
   243  	// handle both those types of errors as well as "real" errors.
   244  	if rc, done := handleCommandError(c, ctx, c.Init(f.Args()), f); done {
   245  		return rc
   246  	}
   247  	if err := c.Run(ctx); err != nil {
   248  		if IsRcPassthroughError(err) {
   249  			return err.(*RcPassthroughError).Code
   250  		}
   251  		if err != ErrSilent {
   252  			fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
   253  		}
   254  		return 1
   255  	}
   256  	return 0
   257  }
   258  
   259  // DefaultContext returns a Context suitable for use in non-hosted situations.
   260  func DefaultContext() (*Context, error) {
   261  	dir, err := os.Getwd()
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	abs, err := filepath.Abs(dir)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	return &Context{
   270  		Dir:    abs,
   271  		Stdin:  os.Stdin,
   272  		Stdout: os.Stdout,
   273  		Stderr: os.Stderr,
   274  	}, nil
   275  }
   276  
   277  // CheckEmpty is a utility function that returns an error if args is not empty.
   278  func CheckEmpty(args []string) error {
   279  	if len(args) != 0 {
   280  		return fmt.Errorf("unrecognized args: %q", args)
   281  	}
   282  	return nil
   283  }
   284  
   285  // ZeroOrOneArgs checks to see that there are zero or one args, and returns
   286  // the value of the arg if provided, or the empty string if not.
   287  func ZeroOrOneArgs(args []string) (string, error) {
   288  	var result string
   289  	if len(args) > 0 {
   290  		result, args = args[0], args[1:]
   291  	}
   292  	if err := CheckEmpty(args); err != nil {
   293  		return "", err
   294  	}
   295  	return result, nil
   296  }