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