-
Notifications
You must be signed in to change notification settings - Fork 0
/
cli.go
380 lines (338 loc) · 11.5 KB
/
cli.go
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
package cli
import (
"fmt"
"strings"
"time"
data "github.com/multiverse-os/cli/data"
)
///////////////////////////////////////////////////////////////////////////////
// Ontology of a command-line interface
///////////////////////////////////////////////////////////////////////////////
//
// global flag command flag parameters (params)
// __|___ __|__ __________|____________
// / \ / \ / \
// app-cli --flag=2 open -f thing template /path/to/file /path/to-file
// \_____/ \__/ \____/
// | | |
// application command subcommand
//
///////////////////////////////////////////////////////////////////////////////
// Alpha Release
// TODO: Expand range of the tests so it test more possible conditions to
// guarantee it works when changes are made
// TODO: change receiver variable names on methods from self to the convention
// TODO: Rewrite the README.md
// TODO: Add ability to access Banner/Spinner (and others) text user interface
// (TUI) tools from actions.
// context.CLI.Spinner()
// TODO: Ability to use ansii via CLI.Screen.Clear(), or CLI.Text.Blue("test")
type App struct {
Name string
Description string
Version Version
Debug bool
Outputs Outputs
GlobalFlags flags
Commands commands
Actions Actions
}
type CLI struct {
Name string
Version Version
//Build Build
Debug bool
Context *Context
Outputs Outputs
Actions Actions
MinimumArgs int // TODO: Not yet implemented
Locale string // TODO: Not yet implented
}
func (c CLI) Log(output ...string) { c.Outputs.Log(DEBUG, output...) }
func (c CLI) Warn(output ...string) { c.Outputs.Log(WARN, output...) }
func (c CLI) Error(output ...string) { c.Outputs.Log(ERROR, output...) }
func (c CLI) Fatal(output ...string) { c.Outputs.Log(FATAL, output...) }
// TODO: Submodule problem need to resolve to get this working, but tis
// advisable to eventually get this
//func (c CLI) Box(message string) string {
// return text.Box(message)
//}
// TODO: Get rid of flag actions by simply catching version or help in a generic
// fallback that looks for these flags. This should also help resolve issues
// requiring hardcoding
func New(appDefinition ...App) (cli *CLI, errs []error) {
// TODO: Clean this up so its not as ugly
app := App{}
if len(appDefinition) != 0 {
app = appDefinition[0]
}
// Validation
errs = append(errs, app.Commands.Validate()...)
errs = append(errs, app.GlobalFlags.Validate()...)
if len(errs) != 0 {
fmt.Println("number of validation errors for flags and commands:", len(errs))
return cli, errs
}
// NOTE: Sensical defaults to avoid error conditions, simplifying library use
if data.IsBlank(app.Name) {
app.Name = "app-cli"
}
if app.Version.undefined() {
app.Version = Version{Major: 0, Minor: 1, Patch: 0}
}
if len(app.Outputs) == 0 {
app.Outputs = append(app.Outputs, TerminalOutput())
}
// NOTE: If a fallback is not set, we render default help template.
if app.Actions.Fallback == nil {
app.Actions.Fallback = HelpCommand
}
cli = &CLI{
Name: app.Name,
Version: app.Version,
Outputs: app.Outputs,
Actions: app.Actions,
//Build: Build{
// CompiledAt: time.Now(),
//},
}
// TODO: Why is Command, Flag
// TODO: This is going to be troublesome come localization
// TODO: Need
// NOTE: Application psuedo-command to store globals
// and simplify logic
appCommand := Command{
Name: app.Name,
Description: app.Description,
Subcommands: app.Commands,
Flags: app.GlobalFlags,
Hidden: true,
Action: app.Actions.Fallback,
}
if !app.Commands.HasCommand("help") {
appCommand.Subcommands.Add(&Command{
Name: "help",
Alias: "h",
Description: "outputs command and flag details",
Action: HelpCommand,
Hidden: true,
})
}
if !app.Commands.HasCommand("version") {
appCommand.Subcommands.Add(&Command{
Name: "version",
Alias: "v",
Description: "outputs version",
Action: VersionCommand,
Hidden: false,
})
}
// TODO: Lets create a VersionFlag and CommandFlag for correcting output
// we want drop the help flag
if !app.GlobalFlags.HasFlag("help") {
hFlag := &Flag{
Command: &appCommand,
Name: "help",
Alias: "h",
Description: "outputs command and flag details",
Hidden: false,
Action: RenderDefaultHelpTemplate,
}
appCommand.Flags.Add(hFlag)
}
if !app.GlobalFlags.HasFlag("version") {
vFlag := &Flag{
Command: &appCommand,
Name: "version",
Alias: "v",
Description: "outputs version",
Hidden: true,
Action: RenderDefaultVersionTemplate,
}
appCommand.Flags.Add(vFlag)
}
cli.Context = &Context{
CLI: cli,
Process: Process(),
Commands: Commands(appCommand),
Params: params{},
Flags: appCommand.Flags,
Arguments: Arguments(appCommand),
Actions: actions{},
}
cli.Context.Command = cli.Context.Commands.First()
return cli, errs
}
// TODO: We could use the BeforeAction hook to convert version and help flags
// into commands. Or rather convert trailing help commands into a flag? Don't
// fallback though on the concepts; just find better solutions
func (cli *CLI) LastArgument() Argument { return cli.Context.Arguments.Last() }
func (cli *CLI) FirstCommand() *Command { return cli.Context.Commands.First() }
func (cli *CLI) LastCommand() *Command { return cli.Context.Commands.Last() }
func (cli *CLI) Parse(arguments []string) *CLI {
defer cli.benchmark(time.Now(), "benmarking argument parsing")
// NOTE
// Skip one because we treat the application a command so it
// can store the global flags. This model avoids a lot of
// extra code
for index, argument := range arguments[1:] {
// Flag parse
// But shouldn't flag parsing go from each command upwards?
// Flag
if flagType, ok := HasFlagPrefix(argument); ok {
argument = flagType.TrimPrefix(argument)
switch flagType {
case Short:
for index, shortFlag := range argument {
// NOTE: Confirm we are not last && next argument is '=' (61) &&
if len(argument) != index+1 && argument[index+1] == 61 {
if flag := cli.Context.Flag(string(shortFlag)); flag != nil {
if flagParam := argument[index+2:]; len(flagParam) != 0 {
flag.Set(flagParam)
}
cli.Context.Arguments = cli.Context.Arguments.Add(flag)
break
}
} else {
if flag := cli.Context.Flag(string(shortFlag)); flag != nil {
// NOTE: If the default value is not boolean or blank, no
// assignment occurs to avoid input failures.
if data.IsBoolean(flag.Default) {
flag.Toggle()
} else if len(flag.Default) == 0 {
flag.SetTrue()
}
}
}
}
case Long:
longFlagParts := strings.Split(argument, "=")
flag := cli.Context.Flag(longFlagParts[0])
//// TODO: Validate (which probably should be setting default)
if flag != nil {
switch len(longFlagParts) {
case 2:
flag.Set(longFlagParts[1])
case 1:
// NOTE
// If we only use default to determine type we are ignoring a few edge
// conditions that will inevitably make this difficult to use
// An Boolean: true type attribute would solve it
// HasNext() check
// TODO: +2 because index starts at 0 len() starts at 1
if data.IsBoolean(flag.Default) || flag.Boolean {
// NOTE IS BOOLEAN
flag.Toggle()
} else if index+2 <= len(arguments[1:]) {
// NOTE HAS NEXT
// NOTE
// We don't need to know if it is a valid flag, just that it is a
// flag
if _, ok := HasFlagPrefix(arguments[index+2]); ok {
flag.SetTrue()
}
if cli.LastCommand().Subcommands.HasCommand(arguments[index+2]) {
// NOTE
// In this condition we are talking about a boolean
flag.SetTrue()
}
flag.Set(arguments[index+2])
} else {
// NOTE
// NOT boolean && No Next
// Should be using default or setting to false
// else here should be assumed boolean
flag.SetTrue()
}
}
cli.Context.Arguments.Add(flag)
} else {
// TODO: There are conditions that land here
// ones with default that not boolean
// and has two parts
// and doesnt have next argument
// one of our issues with the edge condition is we
// get duplicate value in flag and params if we dont
// know if its a boolean or not, and this would be
// cleaned up
}
}
} else {
if command := cli.Context.Command.Subcommand(argument); command != nil {
// Command parse
command.Parent = cli.FirstCommand()
cli.Context.Commands.Add(command)
// TODO: don't we do add here? otherwise what was the poiint?
cli.Context.Flags = append(cli.Context.Flags, command.Flags...)
cli.Context.Arguments = cli.Context.Arguments.Add(cli.FirstCommand())
cli.Context.Command = cli.FirstCommand()
} else if (len(argument) == 4 && argument == "help") ||
(len(argument) == 1 && argument == "h") {
helpCommand := cli.LastCommand().Subcommand("help")
if helpCommand != nil {
// TODO: Why is this the parent? What if we are dealing with
// a subcommand of a subcommand? we would want the subcommand
// not the first command
helpCommand.Parent = cli.FirstCommand()
cli.Context.Commands.Add(helpCommand)
cli.Context.Flags = append(cli.Context.Flags, helpCommand.Flags...)
cli.Context.Arguments = cli.Context.Arguments.Add(
cli.FirstCommand(),
)
// TODO: Wait wtf whattt we are setting parent to the same thing
// as we are setting the fucking command this makes no fucking
// sense
cli.Context.Command = cli.FirstCommand()
break
}
} else {
// Params parse
cli.Context.Params = cli.Context.Params.Add(
NewParam(argument),
)
cli.Context.Arguments = cli.Context.Arguments.Add(
cli.Context.Params.First(),
)
}
}
}
// End of parse
// for the purpose of making it easier to use
// to access in this function in the reverse order.
cli.Context.Arguments = Reverse(cli.Context.Arguments)
cli.Context.Commands = ToCommands(Reverse(cli.Context.Commands.Arguments()))
cli.Context.Params = ToParams(Reverse(cli.Context.Params.Arguments()))
return cli
}
func (cli *CLI) Execute() {
cli.Context.Actions.Add(cli.Actions.OnStart)
// TODO: No what should happen is we go through the global flags and run those
// actions first
// TODO: This all fell apart when we had to hard-code 'help' flag AND
// requiring a skip action assignment for commands
var skipCommandAction bool
for _, command := range cli.Context.Commands {
for _, flag := range command.Flags {
if flag.Action != nil && flag.Param != nil {
cli.Context.Actions = append(cli.Context.Actions, flag.Action)
skipCommandAction = true
break
}
}
}
if !skipCommandAction {
if 0 < len(cli.Context.Commands) {
command := cli.Context.Commands.Last()
if command.Action != nil {
cli.Context.Actions = append(cli.Context.Actions, command.Action)
}
}
}
cli.Context.Actions.Add(cli.Actions.OnExit)
// NOTE: Before handing the developer using the library the context we put
// them in the expected left to right order, despite it being easier for us
defer cli.benchmark(time.Now(), "benmarking action execution")
for _, action := range cli.Context.Actions {
action(cli.Context)
}
}