~rogpeppe/juju-core/azure

« back to all changes in this revision

Viewing changes to cmd/supercommand.go

  • Committer: Roger Peppe
  • Date: 2011-12-15 18:54:31 UTC
  • mfrom: (19.5.4 go-juju-ec2-operations)
  • mto: This revision was merged to the branch mainline in revision 30.
  • Revision ID: roger.peppe@canonical.com-20111215185431-tjuxi6bmg1mswcwg
renameĀ environ->environs

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
        "sort"
10
 
        "strings"
11
 
 
12
 
        "launchpad.net/gnuflag"
13
 
 
14
 
        "launchpad.net/juju-core/log"
15
 
)
16
 
 
17
 
type topic struct {
18
 
        short string
19
 
        long  func() string
20
 
}
21
 
 
22
 
type UnrecognizedCommand struct {
23
 
        Name string
24
 
}
25
 
 
26
 
func (e *UnrecognizedCommand) Error() string {
27
 
        return fmt.Sprintf("unrecognized command: %s", e.Name)
28
 
}
29
 
 
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
33
 
 
34
 
// SuperCommandParams provides a way to have default parameter to the
35
 
// `NewSuperCommand` call.
36
 
type SuperCommandParams struct {
37
 
        Name            string
38
 
        Purpose         string
39
 
        Doc             string
40
 
        Log             *Log
41
 
        MissingCallback MissingCallback
42
 
}
43
 
 
44
 
// NewSuperCommand creates and initializes a new `SuperCommand`, and returns
45
 
// the fully initialized structure.
46
 
func NewSuperCommand(params SuperCommandParams) *SuperCommand {
47
 
        command := &SuperCommand{
48
 
                Name:            params.Name,
49
 
                Purpose:         params.Purpose,
50
 
                Doc:             params.Doc,
51
 
                Log:             params.Log,
52
 
                missingCallback: params.MissingCallback}
53
 
        command.init()
54
 
        return command
55
 
}
56
 
 
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 {
62
 
        CommandBase
63
 
        Name            string
64
 
        Purpose         string
65
 
        Doc             string
66
 
        Log             *Log
67
 
        subcmds         map[string]Command
68
 
        flags           *gnuflag.FlagSet
69
 
        subcmd          Command
70
 
        showHelp        bool
71
 
        missingCallback MissingCallback
72
 
}
73
 
 
74
 
// Because Go doesn't have constructors that initialize the object into a
75
 
// ready state.
76
 
func (c *SuperCommand) init() {
77
 
        if c.subcmds != nil {
78
 
                return
79
 
        }
80
 
        help := &helpCommand{
81
 
                super: c,
82
 
        }
83
 
        help.init()
84
 
        c.subcmds = map[string]Command{
85
 
                "help": help,
86
 
        }
87
 
}
88
 
 
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
92
 
// called.
93
 
func (c *SuperCommand) AddHelpTopic(name, short, long string) {
94
 
        c.subcmds["help"].(*helpCommand).addTopic(name, short, echo(long))
95
 
}
96
 
 
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)
101
 
}
102
 
 
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)
110
 
        }
111
 
}
112
 
 
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))
116
 
        }
117
 
        c.subcmds[name] = subcmd
118
 
}
119
 
 
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"
124
 
        if simple {
125
 
                lineFormat = "%-*s  %s"
126
 
                outputFormat = "%s"
127
 
        }
128
 
        cmds := make([]string, len(c.subcmds))
129
 
        i := 0
130
 
        longest := 0
131
 
        for name := range c.subcmds {
132
 
                if len(name) > longest {
133
 
                        longest = len(name)
134
 
                }
135
 
                cmds[i] = name
136
 
                i++
137
 
        }
138
 
        sort.Strings(cmds)
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
144
 
                }
145
 
                cmds[i] = fmt.Sprintf(lineFormat, longest, name, purpose)
146
 
        }
147
 
        return fmt.Sprintf(outputFormat, strings.Join(cmds, "\n"))
148
 
}
149
 
 
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 {
153
 
        if c.subcmd != nil {
154
 
                info := *c.subcmd.Info()
155
 
                info.Name = fmt.Sprintf("%s %s", c.Name, info.Name)
156
 
                return &info
157
 
        }
158
 
        docParts := []string{}
159
 
        if doc := strings.TrimSpace(c.Doc); doc != "" {
160
 
                docParts = append(docParts, doc)
161
 
        }
162
 
        if cmds := c.describeCommands(false); cmds != "" {
163
 
                docParts = append(docParts, cmds)
164
 
        }
165
 
        return &Info{
166
 
                Name:    c.Name,
167
 
                Args:    "<command> ...",
168
 
                Purpose: c.Purpose,
169
 
                Doc:     strings.Join(docParts, "\n\n"),
170
 
        }
171
 
}
172
 
 
173
 
const helpPurpose = "show help on a command or other topic"
174
 
 
175
 
// SetFlags adds the options that apply to all commands, particularly those
176
 
// due to logging.
177
 
func (c *SuperCommand) SetFlags(f *gnuflag.FlagSet) {
178
 
        if c.Log != nil {
179
 
                c.Log.AddFlags(f)
180
 
        }
181
 
        f.BoolVar(&c.showHelp, "h", false, helpPurpose)
182
 
        f.BoolVar(&c.showHelp, "help", false, "")
183
 
 
184
 
        c.flags = f
185
 
}
186
 
 
187
 
// Init initializes the command for running.
188
 
func (c *SuperCommand) Init(args []string) error {
189
 
        if len(args) == 0 {
190
 
                c.subcmd = c.subcmds["help"]
191
 
                return nil
192
 
        }
193
 
 
194
 
        found := false
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,
200
 
                                superName: c.Name,
201
 
                                name:      args[0],
202
 
                                args:      args[1:],
203
 
                        }
204
 
                        // Yes return here, no Init called on missing Command.
205
 
                        return nil
206
 
                }
207
 
                return fmt.Errorf("unrecognized command: %s %s", c.Name, args[0])
208
 
        }
209
 
        args = args[1:]
210
 
        c.subcmd.SetFlags(c.flags)
211
 
        if err := c.flags.Parse(true, args); err != nil {
212
 
                return err
213
 
        }
214
 
        args = c.flags.Args()
215
 
        if c.showHelp {
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"]
219
 
        }
220
 
        return c.subcmd.Init(args)
221
 
}
222
 
 
223
 
// Run executes the subcommand that was selected in Init.
224
 
func (c *SuperCommand) Run(ctx *Context) error {
225
 
        if c.subcmd == nil {
226
 
                panic("Run: missing subcommand; Init failed or not called")
227
 
        }
228
 
        if c.Log != nil {
229
 
                if err := c.Log.Start(ctx); err != nil {
230
 
                        return err
231
 
                }
232
 
        }
233
 
        err := c.subcmd.Run(ctx)
234
 
        if err != nil && err != ErrSilent {
235
 
                log.Errorf("command failed: %v", err)
236
 
        } else {
237
 
                log.Infof("command finished")
238
 
        }
239
 
        return err
240
 
}
241
 
 
242
 
type missingCommand struct {
243
 
        CommandBase
244
 
        callback  MissingCallback
245
 
        superName string
246
 
        name      string
247
 
        args      []string
248
 
}
249
 
 
250
 
// Missing commands only need to supply Info for the interface, but this is
251
 
// never called.
252
 
func (c *missingCommand) Info() *Info {
253
 
        return nil
254
 
}
255
 
 
256
 
func (c *missingCommand) Run(ctx *Context) error {
257
 
        err := c.callback(ctx, c.name, c.args)
258
 
        _, isUnrecognized := err.(*UnrecognizedCommand)
259
 
        if !isUnrecognized {
260
 
                return err
261
 
        }
262
 
        return &UnrecognizedCommand{c.superName + " " + c.name}
263
 
}
264
 
 
265
 
type helpCommand struct {
266
 
        CommandBase
267
 
        super  *SuperCommand
268
 
        topic  string
269
 
        topics map[string]topic
270
 
}
271
 
 
272
 
func (c *helpCommand) init() {
273
 
        c.topics = map[string]topic{
274
 
                "commands": {
275
 
                        short: "Basic help for all commands",
276
 
                        long:  func() string { return c.super.describeCommands(true) },
277
 
                },
278
 
                "global-options": {
279
 
                        short: "Options common to all commands",
280
 
                        long:  func() string { return c.globalOptions() },
281
 
                },
282
 
                "topics": {
283
 
                        short: "Topic list",
284
 
                        long:  func() string { return c.topicList() },
285
 
                },
286
 
        }
287
 
}
288
 
 
289
 
func echo(s string) func() string {
290
 
        return func() string { return s }
291
 
}
292
 
 
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))
296
 
        }
297
 
        c.topics[name] = topic{short, long}
298
 
}
299
 
 
300
 
func (c *helpCommand) globalOptions() string {
301
 
        buf := &bytes.Buffer{}
302
 
        fmt.Fprintf(buf, `Global Options
303
 
 
304
 
These options may be used with any command, and may appear in front of any
305
 
command.
306
 
 
307
 
`)
308
 
 
309
 
        f := gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
310
 
        c.super.SetFlags(f)
311
 
        f.SetOutput(buf)
312
 
        f.PrintDefaults()
313
 
        return buf.String()
314
 
}
315
 
 
316
 
func (c *helpCommand) topicList() string {
317
 
        topics := make([]string, len(c.topics))
318
 
        i := 0
319
 
        longest := 0
320
 
        for name := range c.topics {
321
 
                if len(name) > longest {
322
 
                        longest = len(name)
323
 
                }
324
 
                topics[i] = name
325
 
                i++
326
 
        }
327
 
        sort.Strings(topics)
328
 
        for i, name := range topics {
329
 
                shortHelp := c.topics[name].short
330
 
                topics[i] = fmt.Sprintf("%-*s  %s", longest, name, shortHelp)
331
 
        }
332
 
        return fmt.Sprintf("%s", strings.Join(topics, "\n"))
333
 
}
334
 
 
335
 
func (c *helpCommand) Info() *Info {
336
 
        return &Info{
337
 
                Name:    "help",
338
 
                Args:    "[topic]",
339
 
                Purpose: helpPurpose,
340
 
                Doc: `
341
 
See also: topics
342
 
`,
343
 
        }
344
 
}
345
 
 
346
 
func (c *helpCommand) Init(args []string) error {
347
 
        switch len(args) {
348
 
        case 0:
349
 
        case 1:
350
 
                c.topic = args[0]
351
 
        default:
352
 
                return fmt.Errorf("extra arguments to command help: %q", args[2:])
353
 
        }
354
 
        return nil
355
 
}
356
 
 
357
 
func (c *helpCommand) Run(ctx *Context) error {
358
 
        // If there is no help topic specified, print basic usage.
359
 
        if c.topic == "" {
360
 
                if _, ok := c.topics["basics"]; ok {
361
 
                        c.topic = "basics"
362
 
                } else {
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.
366
 
                        c.super.subcmd = nil
367
 
 
368
 
                        info := c.super.Info()
369
 
                        f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
370
 
                        c.SetFlags(f)
371
 
                        ctx.Stdout.Write(info.Help(f))
372
 
                        return nil
373
 
                }
374
 
        }
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)
380
 
                helpcmd.SetFlags(f)
381
 
                ctx.Stdout.Write(info.Help(f))
382
 
                return nil
383
 
        }
384
 
        // Look to see if the topic is a registered topic.
385
 
        topic, ok := c.topics[c.topic]
386
 
        if ok {
387
 
                fmt.Fprintf(ctx.Stdout, "%s\n", strings.TrimSpace(topic.long()))
388
 
                return nil
389
 
        }
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,
395
 
                        name:      c.topic,
396
 
                        args:      []string{"--", "--help"},
397
 
                }
398
 
                err := subcmd.Run(ctx)
399
 
                _, isUnrecognized := err.(*UnrecognizedCommand)
400
 
                if !isUnrecognized {
401
 
                        return err
402
 
                }
403
 
        }
404
 
        return fmt.Errorf("unknown command or topic for %s", c.topic)
405
 
}