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  }