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 }