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 }