launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/cmd/supercommand.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 "fmt" 9 "io/ioutil" 10 "sort" 11 "strings" 12 13 "github.com/loggo/loggo" 14 15 "launchpad.net/gnuflag" 16 ) 17 18 var logger = loggo.GetLogger("juju.cmd") 19 20 type topic struct { 21 short string 22 long func() string 23 } 24 25 type UnrecognizedCommand struct { 26 Name string 27 } 28 29 func (e *UnrecognizedCommand) Error() string { 30 return fmt.Sprintf("unrecognized command: %s", e.Name) 31 } 32 33 // MissingCallback defines a function that will be used by the SuperCommand if 34 // the requested subcommand isn't found. 35 type MissingCallback func(ctx *Context, subcommand string, args []string) error 36 37 // SuperCommandParams provides a way to have default parameter to the 38 // `NewSuperCommand` call. 39 type SuperCommandParams struct { 40 UsagePrefix string 41 Name string 42 Purpose string 43 Doc string 44 Log *Log 45 MissingCallback MissingCallback 46 } 47 48 // NewSuperCommand creates and initializes a new `SuperCommand`, and returns 49 // the fully initialized structure. 50 func NewSuperCommand(params SuperCommandParams) *SuperCommand { 51 command := &SuperCommand{ 52 Name: params.Name, 53 Purpose: params.Purpose, 54 Doc: params.Doc, 55 Log: params.Log, 56 usagePrefix: params.UsagePrefix, 57 missingCallback: params.MissingCallback} 58 command.init() 59 return command 60 } 61 62 // SuperCommand is a Command that selects a subcommand and assumes its 63 // properties; any command line arguments that were not used in selecting 64 // the subcommand are passed down to it, and to Run a SuperCommand is to run 65 // its selected subcommand. 66 type SuperCommand struct { 67 CommandBase 68 Name string 69 Purpose string 70 Doc string 71 Log *Log 72 usagePrefix string 73 subcmds map[string]Command 74 commonflags *gnuflag.FlagSet 75 flags *gnuflag.FlagSet 76 subcmd Command 77 showHelp bool 78 showDescription bool 79 showVersion bool 80 missingCallback MissingCallback 81 } 82 83 // IsSuperCommand implements Command.IsSuperCommand 84 func (c *SuperCommand) IsSuperCommand() bool { 85 return true 86 } 87 88 // Because Go doesn't have constructors that initialize the object into a 89 // ready state. 90 func (c *SuperCommand) init() { 91 if c.subcmds != nil { 92 return 93 } 94 help := &helpCommand{ 95 super: c, 96 } 97 help.init() 98 c.subcmds = map[string]Command{ 99 "help": help, 100 } 101 } 102 103 // AddHelpTopic adds a new help topic with the description being the short 104 // param, and the full text being the long param. The description is shown in 105 // 'help topics', and the full text is shown when the command 'help <name>' is 106 // called. 107 func (c *SuperCommand) AddHelpTopic(name, short, long string) { 108 c.subcmds["help"].(*helpCommand).addTopic(name, short, echo(long)) 109 } 110 111 // AddHelpTopicCallback adds a new help topic with the description being the 112 // short param, and the full text being defined by the callback function. 113 func (c *SuperCommand) AddHelpTopicCallback(name, short string, longCallback func() string) { 114 c.subcmds["help"].(*helpCommand).addTopic(name, short, longCallback) 115 } 116 117 // Register makes a subcommand available for use on the command line. The 118 // command will be available via its own name, and via any supplied aliases. 119 func (c *SuperCommand) Register(subcmd Command) { 120 info := subcmd.Info() 121 c.insert(info.Name, subcmd) 122 for _, name := range info.Aliases { 123 c.insert(name, subcmd) 124 } 125 } 126 127 func (c *SuperCommand) insert(name string, subcmd Command) { 128 if _, found := c.subcmds[name]; found || name == "help" { 129 panic(fmt.Sprintf("command already registered: %s", name)) 130 } 131 c.subcmds[name] = subcmd 132 } 133 134 // describeCommands returns a short description of each registered subcommand. 135 func (c *SuperCommand) describeCommands(simple bool) string { 136 var lineFormat = " %-*s - %s" 137 var outputFormat = "commands:\n%s" 138 if simple { 139 lineFormat = "%-*s %s" 140 outputFormat = "%s" 141 } 142 cmds := make([]string, len(c.subcmds)) 143 i := 0 144 longest := 0 145 for name := range c.subcmds { 146 if len(name) > longest { 147 longest = len(name) 148 } 149 cmds[i] = name 150 i++ 151 } 152 sort.Strings(cmds) 153 for i, name := range cmds { 154 info := c.subcmds[name].Info() 155 purpose := info.Purpose 156 if name != info.Name { 157 purpose = "alias for " + info.Name 158 } 159 cmds[i] = fmt.Sprintf(lineFormat, longest, name, purpose) 160 } 161 return fmt.Sprintf(outputFormat, strings.Join(cmds, "\n")) 162 } 163 164 // Info returns a description of the currently selected subcommand, or of the 165 // SuperCommand itself if no subcommand has been specified. 166 func (c *SuperCommand) Info() *Info { 167 if c.subcmd != nil { 168 info := *c.subcmd.Info() 169 info.Name = fmt.Sprintf("%s %s", c.Name, info.Name) 170 return &info 171 } 172 docParts := []string{} 173 if doc := strings.TrimSpace(c.Doc); doc != "" { 174 docParts = append(docParts, doc) 175 } 176 if cmds := c.describeCommands(false); cmds != "" { 177 docParts = append(docParts, cmds) 178 } 179 return &Info{ 180 Name: c.Name, 181 Args: "<command> ...", 182 Purpose: c.Purpose, 183 Doc: strings.Join(docParts, "\n\n"), 184 } 185 } 186 187 const helpPurpose = "show help on a command or other topic" 188 189 // SetCommonFlags creates a new "commonflags" flagset, whose 190 // flags are shared with the argument f; this enables us to 191 // add non-global flags to f, which do not carry into subcommands. 192 func (c *SuperCommand) SetCommonFlags(f *gnuflag.FlagSet) { 193 if c.Log != nil { 194 c.Log.AddFlags(f) 195 } 196 f.BoolVar(&c.showHelp, "h", false, helpPurpose) 197 f.BoolVar(&c.showHelp, "help", false, "") 198 // In the case where we are providing the basis for a plugin, 199 // plugins are required to support the --description argument. 200 // The Purpose attribute will be printed (if defined), allowing 201 // plugins to provide a sensible line of text for 'juju help plugins'. 202 f.BoolVar(&c.showDescription, "description", false, "") 203 c.commonflags = gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError) 204 c.commonflags.SetOutput(ioutil.Discard) 205 f.VisitAll(func(flag *gnuflag.Flag) { 206 c.commonflags.Var(flag.Value, flag.Name, flag.Usage) 207 }) 208 } 209 210 // SetFlags adds the options that apply to all commands, particularly those 211 // due to logging. 212 func (c *SuperCommand) SetFlags(f *gnuflag.FlagSet) { 213 c.SetCommonFlags(f) 214 // Only flags set by SetCommonFlags are passed on to subcommands. 215 // Any flags added below only take effect when no subcommand is 216 // specified (e.g. juju --version). 217 f.BoolVar(&c.showVersion, "version", false, "Show the version of juju") 218 c.flags = f 219 } 220 221 // For a SuperCommand, we want to parse the args with 222 // allowIntersperse=false. This will mean that the args may contain other 223 // options that haven't been defined yet, and that only options that relate 224 // to the SuperCommand itself can come prior to the subcommand name. 225 func (c *SuperCommand) AllowInterspersedFlags() bool { 226 return false 227 } 228 229 // Init initializes the command for running. 230 func (c *SuperCommand) Init(args []string) error { 231 if c.showDescription { 232 return CheckEmpty(args) 233 } 234 if len(args) == 0 { 235 c.subcmd = c.subcmds["help"] 236 return nil 237 } 238 239 found := false 240 // Look for the command. 241 if c.subcmd, found = c.subcmds[args[0]]; !found { 242 if c.missingCallback != nil { 243 c.subcmd = &missingCommand{ 244 callback: c.missingCallback, 245 superName: c.Name, 246 name: args[0], 247 args: args[1:], 248 } 249 // Yes return here, no Init called on missing Command. 250 return nil 251 } 252 return fmt.Errorf("unrecognized command: %s %s", c.Name, args[0]) 253 } 254 args = args[1:] 255 if c.subcmd.IsSuperCommand() { 256 f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError) 257 f.SetOutput(ioutil.Discard) 258 c.subcmd.SetFlags(f) 259 } else { 260 c.subcmd.SetFlags(c.commonflags) 261 } 262 if err := c.commonflags.Parse(c.subcmd.AllowInterspersedFlags(), args); err != nil { 263 return err 264 } 265 args = c.commonflags.Args() 266 if c.showHelp { 267 // We want to treat help for the command the same way we would if we went "help foo". 268 args = []string{c.subcmd.Info().Name} 269 c.subcmd = c.subcmds["help"] 270 } 271 return c.subcmd.Init(args) 272 } 273 274 // Run executes the subcommand that was selected in Init. 275 func (c *SuperCommand) Run(ctx *Context) error { 276 if c.showDescription { 277 if c.Purpose != "" { 278 fmt.Fprintf(ctx.Stdout, "%s\n", c.Purpose) 279 } else { 280 fmt.Fprintf(ctx.Stdout, "%s: no description available\n", c.Info().Name) 281 } 282 return nil 283 } 284 if c.subcmd == nil { 285 panic("Run: missing subcommand; Init failed or not called") 286 } 287 if c.Log != nil { 288 if err := c.Log.Start(ctx); err != nil { 289 return err 290 } 291 } 292 err := c.subcmd.Run(ctx) 293 if err != nil && err != ErrSilent { 294 logger.Errorf("%v", err) 295 // Now that this has been logged, don't log again in cmd.Main. 296 if !IsRcPassthroughError(err) { 297 err = ErrSilent 298 } 299 } else { 300 logger.Infof("command finished") 301 } 302 return err 303 } 304 305 type missingCommand struct { 306 CommandBase 307 callback MissingCallback 308 superName string 309 name string 310 args []string 311 } 312 313 // Missing commands only need to supply Info for the interface, but this is 314 // never called. 315 func (c *missingCommand) Info() *Info { 316 return nil 317 } 318 319 func (c *missingCommand) Run(ctx *Context) error { 320 err := c.callback(ctx, c.name, c.args) 321 _, isUnrecognized := err.(*UnrecognizedCommand) 322 if !isUnrecognized { 323 return err 324 } 325 return &UnrecognizedCommand{c.superName + " " + c.name} 326 } 327 328 type helpCommand struct { 329 CommandBase 330 super *SuperCommand 331 topic string 332 topicArgs []string 333 topics map[string]topic 334 } 335 336 func (c *helpCommand) init() { 337 c.topics = map[string]topic{ 338 "commands": { 339 short: "Basic help for all commands", 340 long: func() string { return c.super.describeCommands(true) }, 341 }, 342 "global-options": { 343 short: "Options common to all commands", 344 long: func() string { return c.globalOptions() }, 345 }, 346 "topics": { 347 short: "Topic list", 348 long: func() string { return c.topicList() }, 349 }, 350 } 351 } 352 353 func echo(s string) func() string { 354 return func() string { return s } 355 } 356 357 func (c *helpCommand) addTopic(name, short string, long func() string) { 358 if _, found := c.topics[name]; found { 359 panic(fmt.Sprintf("help topic already added: %s", name)) 360 } 361 c.topics[name] = topic{short, long} 362 } 363 364 func (c *helpCommand) globalOptions() string { 365 buf := &bytes.Buffer{} 366 fmt.Fprintf(buf, `Global Options 367 368 These options may be used with any command, and may appear in front of any 369 command. 370 371 `) 372 373 f := gnuflag.NewFlagSet("", gnuflag.ContinueOnError) 374 c.super.SetCommonFlags(f) 375 f.SetOutput(buf) 376 f.PrintDefaults() 377 return buf.String() 378 } 379 380 func (c *helpCommand) topicList() string { 381 topics := make([]string, len(c.topics)) 382 i := 0 383 longest := 0 384 for name := range c.topics { 385 if len(name) > longest { 386 longest = len(name) 387 } 388 topics[i] = name 389 i++ 390 } 391 sort.Strings(topics) 392 for i, name := range topics { 393 shortHelp := c.topics[name].short 394 topics[i] = fmt.Sprintf("%-*s %s", longest, name, shortHelp) 395 } 396 return fmt.Sprintf("%s", strings.Join(topics, "\n")) 397 } 398 399 func (c *helpCommand) Info() *Info { 400 return &Info{ 401 Name: "help", 402 Args: "[topic]", 403 Purpose: helpPurpose, 404 Doc: ` 405 See also: topics 406 `, 407 } 408 } 409 410 func (c *helpCommand) Init(args []string) error { 411 switch len(args) { 412 case 0: 413 case 1: 414 c.topic = args[0] 415 default: 416 if c.super.missingCallback == nil { 417 return fmt.Errorf("extra arguments to command help: %q", args[1:]) 418 } else { 419 c.topic = args[0] 420 c.topicArgs = args[1:] 421 } 422 } 423 return nil 424 } 425 426 func (c *helpCommand) Run(ctx *Context) error { 427 if c.super.showVersion { 428 var v VersionCommand 429 v.SetFlags(c.super.flags) 430 v.Init(nil) 431 return v.Run(ctx) 432 } 433 434 // If there is no help topic specified, print basic usage. 435 if c.topic == "" { 436 if _, ok := c.topics["basics"]; ok { 437 c.topic = "basics" 438 } else { 439 // At this point, "help" is selected as the SuperCommand's 440 // sub-command, but we want the info to be printed 441 // as if there was nothing selected. 442 c.super.subcmd = nil 443 444 info := c.super.Info() 445 f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) 446 c.SetFlags(f) 447 ctx.Stdout.Write(info.Help(f)) 448 return nil 449 } 450 } 451 // If the topic is a registered subcommand, then run the help command with it 452 if helpcmd, ok := c.super.subcmds[c.topic]; ok { 453 info := helpcmd.Info() 454 info.Name = fmt.Sprintf("%s %s", c.super.Name, info.Name) 455 if c.super.usagePrefix != "" { 456 info.Name = fmt.Sprintf("%s %s", c.super.usagePrefix, info.Name) 457 } 458 f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) 459 helpcmd.SetFlags(f) 460 ctx.Stdout.Write(info.Help(f)) 461 return nil 462 } 463 // Look to see if the topic is a registered topic. 464 topic, ok := c.topics[c.topic] 465 if ok { 466 fmt.Fprintf(ctx.Stdout, "%s\n", strings.TrimSpace(topic.long())) 467 return nil 468 } 469 // If we have a missing callback, call that with --help 470 if c.super.missingCallback != nil { 471 helpArgs := []string{"--help"} 472 if len(c.topicArgs) > 0 { 473 helpArgs = append(helpArgs, c.topicArgs...) 474 } 475 subcmd := &missingCommand{ 476 callback: c.super.missingCallback, 477 superName: c.super.Name, 478 name: c.topic, 479 args: helpArgs, 480 } 481 err := subcmd.Run(ctx) 482 _, isUnrecognized := err.(*UnrecognizedCommand) 483 if !isUnrecognized { 484 return err 485 } 486 } 487 return fmt.Errorf("unknown command or topic for %s", c.topic) 488 }