-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathrunner.go
More file actions
270 lines (252 loc) · 8.13 KB
/
runner.go
File metadata and controls
270 lines (252 loc) · 8.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package cli
import (
"github.com/Masterminds/cookoo"
"flag"
"fmt"
"os"
"strings"
)
// New creates a new CLI instance.
//
// It takes a flagset for parsing command line options, and creates a new
// CLI application initialized. Flags are placed into the context.
//
// By default, the `cookoo.BasicRequestResolver` is used for resolving request.
// This works well with the subcommand model of delegating commands.
//
// If a '@startup' route is inserted into the registry, it will be run first
// upon any call to `Router.HandleRequest`. If not, the default startup will
// be run. That routine includes displaying help text if the -h or -help
// flags are passed in.
//
// The `summary` is a one line explanation of the program used in help text.
//
// The `usage` is a detailed help message, often several paragraphs.
//
// The `globalFlags` are a `flag.FlagSet` for the top level of this program. If
// you use subcommands and want subcommand-specific flags, use the `ParseArgs`
// command in this package.
//
// Typical usage:
//
// package main
//
// import(
// "github.com/Masterminds/cookoo"
// "github.com/Masterminds/cookoo/cli"
// "flag"
// )
//
// var Summary := "A program that does stuff."
// var Description := `Full text description goes here.`
// func main() {
// flags := flag.FlagSet("global", flag.PanicOnError)
// // Define any flags here...
// flags.Bool("h", false, "Show help text")
//
// reg, router, cxt := cookoo.Cookoo()
// reg.Route("hello", "Does nothing")
//
// cli.New(reg, router, cxt).Help(Summary, Description, flags).Run("hello")
// }
//
// The simple program above can be run any of these ways:
//
// $ mycli # Will run 'hello', which does nothing
// $ mycli -h # Will show help
//
// If we were to substitute `RunSubcommand` instead of `Run`:
//
// cli.New(reg, router, cxt).Help(Summary, Description, flags).RunSubcommand()
//
// The above would do the following:
// $ mycli # Will show help
// $ mycli -h # Will show help
// $ mycli help # Will show help
// $ mycli hello # Will run hte "hello" route, which does nothing.
//
func New(reg *cookoo.Registry, router *cookoo.Router, cxt cookoo.Context) *Runner {
return &Runner{reg: reg, router: router, cxt: cxt}
}
// Runner is a CLI runner.
//
// It provides a nice abstraction for simply and easily running CLI
// commands.
type Runner struct {
reg *cookoo.Registry
router *cookoo.Router
cxt cookoo.Context
summary, usage string
flags *flag.FlagSet
}
// Help sets the help text and support flags for the app.
//
// It is strongly advised to use this function for all CLI runner apps.
func (r *Runner) Help(summary, usage string, flags *flag.FlagSet) *Runner {
r.summary = summary
r.usage = usage
r.flags = flags
return r
}
// Subcommand sets up the basics for a subcommand.
//
// It creates a route complete with help and flags parser, then returns that
// route for you to add commands.
//
// Example:
// package main
// import (
// "github.com/Masterminds/cookoo"
// "github.com/Masterminds/cookoo/cli"
// "github.com/Masterminds/cookoo/fmt"
//
// "flag"
// )
//
// func main() {
// reg, router, cxt := cookoo.Cookoo()
//
// // global flags
// flags := flag.NewFlagSet("global", flag.PanicOnError)
// flags.Bool("h", false, "Print help")
//
// // Create a new app
// app := cli.New(reg, router, cxt).Help("Test", "test -h", flags)
//
// // Create a new subcommand on that app
// app.Subcommand("test", "A test route.", "example test", nil).
// Does(fmt.Println, "out").Using("content").WithDefault("Hello World")
//
// helloFlags := flag.NewFlagSet("test", flag.ContinueOnError)
// helloFlags.Bool("h", false, "Print help")
// helloFlags.String("n", "World!", "A name to greet.")
// app.Subcommand("hello", "Print hello.", "example hello -n Matt", helloFlags).
// Does(fmt.Printf, "out").
// Using("format").WithDefault("Hello %s\n").
// Using("0").WithDefault("World").From("cxt:n")
//
// // Run the app, and let it figure out which subcommand to run.
// app.RunSubcommand()
// }
//
// The above declares two subcommands: 'test' and 'hello'. If the flags argument
// is nil, the Subcommand will automatically create a default flagset with
// '-h' mapped to the help.
//
// Any remaining arguments are placed into the context as "subcommand.Args"
func (r *Runner) Subcommand(name, summary, usage string, flags *flag.FlagSet) *cookoo.Registry {
if flags == nil {
flags = flag.NewFlagSet("nada", flag.ContinueOnError)
flags.Bool("h", false, "Show help")
}
return r.reg.Route(name, summary).
Does(ShiftArgs, "_").Using("n").WithDefault(1).
Does(ParseArgs, "subcommand.Args").
Using("flagset").WithDefault(flags).
Using("args").From("cxt:os.Args").
Does(ShowHelp, "subcommandHelp").
Using("show").From("cxt:h").
Using("summary").WithDefault(summary).
Using("usage").WithDefault(usage).
Using("flags").WithDefault(flags)
}
func (r *Runner) startup() {
if r.flags == nil {
r.flags = flag.NewFlagSet("globalFlags", flag.PanicOnError)
r.flags.Bool("h", false, "Show this help text.")
r.flags.Bool("help", false, "Show this help text.")
}
r.cxt.Put("globalFlags", r.flags)
r.cxt.Put("os.Args", os.Args)
// Allow route to be overwritten.
if _, ok := r.reg.RouteSpec("@startup"); !ok {
r.reg.Route("@startup", "Prepare to run a route.").
Does(ShiftArgs, "_").Using("n").WithDefault(1).
Does(ParseArgs, "runner.Args").
Using("flagset").WithDefault(r.flags).
Using("args").From("cxt:os.Args").
Does(ShowHelp, "help").
Using("show").From("cxt:h").
Using("summary").WithDefault(r.summary).
Using("usage").WithDefault(r.usage).
Using("flags").WithDefault(r.flags).
Using("subcommands").From("cxt:subcommandHelp").
Does(ShowHelp, "-help"). // Stupid hack. FIXME.
Using("show").From("cxt:help").
Using("summary").WithDefault(r.summary).
Using("usage").WithDefault(r.usage).
Using("flags").WithDefault(r.flags).
Using("subcommands").From("cxt:subcommandHelp")
}
if _, ok := r.reg.RouteSpec("@subcommand"); !ok {
r.reg.Route("@subcommand", "Startup and run subcommand").
Includes("@startup").
Does(RunSubcommand, "subcommand").
Using("default").WithDefault("help").From("cxt:defaultRoute").
Using("offset").WithDefault(0).
Using("args").From("cxt:runner.Args").
Using("ignoreRoutes").WithDefault([]string{"@startup", "@subcommand"})
}
if _, ok := r.reg.RouteSpec("help"); !ok {
r.reg.Route("help", "Show help.").
Does(ShowHelp, "help").
Using("show").WithDefault(true).
Using("summary").WithDefault(r.summary).
Using("usage").WithDefault(r.usage).
Using("flags").WithDefault(r.flags)
}
}
// Run runs a given route.
//
// It first runs the '@startup' route, and then runs whatever the named route
// is.
//
// If the flags `-h` or `-help` are specified, then the presence of those
// flags will automatically trigger help text.
//
// Additionally, the command `help` is predefined to generate help text.
func (r *Runner) Run(route string) error {
r.startup()
if err := r.router.HandleRequest("@startup", r.cxt, false); err != nil {
fmt.Printf("Failed to startup: %s", err)
os.Exit(1)
//return err
}
if r.cxt.Get("help", false).(bool) {
return nil
}
// FIXME: Hack
if r.cxt.Get("-help", false).(bool) {
return nil
}
return r.router.HandleRequest(route, r.cxt, true)
}
// RunSubcommand uses the first non-flag argument in args as a route name.
//
// For example:
// $ mycli -foo=bar myroute
//
// The above will see 'myroute' as a subcommand, and match it to a route named
// 'subcommand'. In the event that the route is not present, the help text
// will be displayed.
//
//
func (r *Runner) RunSubcommand() error {
r.startup()
shelp := subcommandHelp(r.reg)
r.cxt.Put("subcommandHelp", shelp)
return r.router.HandleRequest("@subcommand", r.cxt, false)
}
func subcommandHelp(reg *cookoo.Registry) string {
names := reg.RouteNames()
helptext := make([]string, 0, len(names))
for _, name := range names {
if strings.HasPrefix(name, "@") {
continue
}
rs, _ := reg.RouteSpec(name)
help := fmt.Sprintf("\t%s: %s", name, cookoo.RouteDetails(rs).Description())
helptext = append(helptext, help)
}
return strings.Join(helptext, "\n")
}