1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
12
"launchpad.net/gnuflag"
14
"launchpad.net/juju-core/log"
22
type UnrecognizedCommand struct {
26
func (e *UnrecognizedCommand) Error() string {
27
return fmt.Sprintf("unrecognized command: %s", e.Name)
30
// MissingCallback defines a function that will be used by the SuperCommand if
31
// the requested subcommand isn't found.
32
type MissingCallback func(ctx *Context, subcommand string, args []string) error
34
// SuperCommandParams provides a way to have default parameter to the
35
// `NewSuperCommand` call.
36
type SuperCommandParams struct {
41
MissingCallback MissingCallback
44
// NewSuperCommand creates and initializes a new `SuperCommand`, and returns
45
// the fully initialized structure.
46
func NewSuperCommand(params SuperCommandParams) *SuperCommand {
47
command := &SuperCommand{
49
Purpose: params.Purpose,
52
missingCallback: params.MissingCallback}
57
// SuperCommand is a Command that selects a subcommand and assumes its
58
// properties; any command line arguments that were not used in selecting
59
// the subcommand are passed down to it, and to Run a SuperCommand is to run
60
// its selected subcommand.
61
type SuperCommand struct {
67
subcmds map[string]Command
68
flags *gnuflag.FlagSet
71
missingCallback MissingCallback
74
// Because Go doesn't have constructors that initialize the object into a
76
func (c *SuperCommand) init() {
84
c.subcmds = map[string]Command{
89
// AddHelpTopic adds a new help topic with the description being the short
90
// param, and the full text being the long param. The description is shown in
91
// 'help topics', and the full text is shown when the command 'help <name>' is
93
func (c *SuperCommand) AddHelpTopic(name, short, long string) {
94
c.subcmds["help"].(*helpCommand).addTopic(name, short, echo(long))
97
// AddHelpTopicCallback adds a new help topic with the description being the
98
// short param, and the full text being defined by the callback function.
99
func (c *SuperCommand) AddHelpTopicCallback(name, short string, longCallback func() string) {
100
c.subcmds["help"].(*helpCommand).addTopic(name, short, longCallback)
103
// Register makes a subcommand available for use on the command line. The
104
// command will be available via its own name, and via any supplied aliases.
105
func (c *SuperCommand) Register(subcmd Command) {
106
info := subcmd.Info()
107
c.insert(info.Name, subcmd)
108
for _, name := range info.Aliases {
109
c.insert(name, subcmd)
113
func (c *SuperCommand) insert(name string, subcmd Command) {
114
if _, found := c.subcmds[name]; found || name == "help" {
115
panic(fmt.Sprintf("command already registered: %s", name))
117
c.subcmds[name] = subcmd
120
// describeCommands returns a short description of each registered subcommand.
121
func (c *SuperCommand) describeCommands(simple bool) string {
122
var lineFormat = " %-*s - %s"
123
var outputFormat = "commands:\n%s"
125
lineFormat = "%-*s %s"
128
cmds := make([]string, len(c.subcmds))
131
for name := range c.subcmds {
132
if len(name) > longest {
139
for i, name := range cmds {
140
info := c.subcmds[name].Info()
141
purpose := info.Purpose
142
if name != info.Name {
143
purpose = "alias for " + info.Name
145
cmds[i] = fmt.Sprintf(lineFormat, longest, name, purpose)
147
return fmt.Sprintf(outputFormat, strings.Join(cmds, "\n"))
150
// Info returns a description of the currently selected subcommand, or of the
151
// SuperCommand itself if no subcommand has been specified.
152
func (c *SuperCommand) Info() *Info {
154
info := *c.subcmd.Info()
155
info.Name = fmt.Sprintf("%s %s", c.Name, info.Name)
158
docParts := []string{}
159
if doc := strings.TrimSpace(c.Doc); doc != "" {
160
docParts = append(docParts, doc)
162
if cmds := c.describeCommands(false); cmds != "" {
163
docParts = append(docParts, cmds)
167
Args: "<command> ...",
169
Doc: strings.Join(docParts, "\n\n"),
173
const helpPurpose = "show help on a command or other topic"
175
// SetFlags adds the options that apply to all commands, particularly those
177
func (c *SuperCommand) SetFlags(f *gnuflag.FlagSet) {
181
f.BoolVar(&c.showHelp, "h", false, helpPurpose)
182
f.BoolVar(&c.showHelp, "help", false, "")
187
// Init initializes the command for running.
188
func (c *SuperCommand) Init(args []string) error {
190
c.subcmd = c.subcmds["help"]
195
// Look for the command.
196
if c.subcmd, found = c.subcmds[args[0]]; !found {
197
if c.missingCallback != nil {
198
c.subcmd = &missingCommand{
199
callback: c.missingCallback,
204
// Yes return here, no Init called on missing Command.
207
return fmt.Errorf("unrecognized command: %s %s", c.Name, args[0])
210
c.subcmd.SetFlags(c.flags)
211
if err := c.flags.Parse(true, args); err != nil {
214
args = c.flags.Args()
216
// We want to treat help for the command the same way we would if we went "help foo".
217
args = []string{c.subcmd.Info().Name}
218
c.subcmd = c.subcmds["help"]
220
return c.subcmd.Init(args)
223
// Run executes the subcommand that was selected in Init.
224
func (c *SuperCommand) Run(ctx *Context) error {
226
panic("Run: missing subcommand; Init failed or not called")
229
if err := c.Log.Start(ctx); err != nil {
233
err := c.subcmd.Run(ctx)
234
if err != nil && err != ErrSilent {
235
log.Errorf("command failed: %v", err)
237
log.Infof("command finished")
242
type missingCommand struct {
244
callback MissingCallback
250
// Missing commands only need to supply Info for the interface, but this is
252
func (c *missingCommand) Info() *Info {
256
func (c *missingCommand) Run(ctx *Context) error {
257
err := c.callback(ctx, c.name, c.args)
258
_, isUnrecognized := err.(*UnrecognizedCommand)
262
return &UnrecognizedCommand{c.superName + " " + c.name}
265
type helpCommand struct {
269
topics map[string]topic
272
func (c *helpCommand) init() {
273
c.topics = map[string]topic{
275
short: "Basic help for all commands",
276
long: func() string { return c.super.describeCommands(true) },
279
short: "Options common to all commands",
280
long: func() string { return c.globalOptions() },
284
long: func() string { return c.topicList() },
289
func echo(s string) func() string {
290
return func() string { return s }
293
func (c *helpCommand) addTopic(name, short string, long func() string) {
294
if _, found := c.topics[name]; found {
295
panic(fmt.Sprintf("help topic already added: %s", name))
297
c.topics[name] = topic{short, long}
300
func (c *helpCommand) globalOptions() string {
301
buf := &bytes.Buffer{}
302
fmt.Fprintf(buf, `Global Options
304
These options may be used with any command, and may appear in front of any
309
f := gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
316
func (c *helpCommand) topicList() string {
317
topics := make([]string, len(c.topics))
320
for name := range c.topics {
321
if len(name) > longest {
328
for i, name := range topics {
329
shortHelp := c.topics[name].short
330
topics[i] = fmt.Sprintf("%-*s %s", longest, name, shortHelp)
332
return fmt.Sprintf("%s", strings.Join(topics, "\n"))
335
func (c *helpCommand) Info() *Info {
339
Purpose: helpPurpose,
346
func (c *helpCommand) Init(args []string) error {
352
return fmt.Errorf("extra arguments to command help: %q", args[2:])
357
func (c *helpCommand) Run(ctx *Context) error {
358
// If there is no help topic specified, print basic usage.
360
if _, ok := c.topics["basics"]; ok {
363
// At this point, "help" is selected as the SuperCommand's
364
// sub-command, but we want the info to be printed
365
// as if there was nothing selected.
368
info := c.super.Info()
369
f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
371
ctx.Stdout.Write(info.Help(f))
375
// If the topic is a registered subcommand, then run the help command with it
376
if helpcmd, ok := c.super.subcmds[c.topic]; ok {
377
info := helpcmd.Info()
378
info.Name = fmt.Sprintf("%s %s", c.super.Name, info.Name)
379
f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
381
ctx.Stdout.Write(info.Help(f))
384
// Look to see if the topic is a registered topic.
385
topic, ok := c.topics[c.topic]
387
fmt.Fprintf(ctx.Stdout, "%s\n", strings.TrimSpace(topic.long()))
390
// If we have a missing callback, call that with --help
391
if c.super.missingCallback != nil {
392
subcmd := &missingCommand{
393
callback: c.super.missingCallback,
394
superName: c.super.Name,
396
args: []string{"--", "--help"},
398
err := subcmd.Run(ctx)
399
_, isUnrecognized := err.(*UnrecognizedCommand)
404
return fmt.Errorf("unknown command or topic for %s", c.topic)