-
-
Notifications
You must be signed in to change notification settings - Fork 10
/
user.go
401 lines (362 loc) · 14.3 KB
/
user.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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// This file is part of go-getoptions.
//
// Copyright (C) 2015-2024 David Gamba Rios
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// TODO: Handle uncomplete options
package getoptions
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/DavidGamba/go-getoptions/internal/help"
"github.com/DavidGamba/go-getoptions/internal/option"
"github.com/DavidGamba/go-getoptions/text"
)
// Logger instance set to `io.Discard` by default.
// Enable debug logging by setting: `Logger.SetOutput(os.Stderr)`.
var Logger = log.New(io.Discard, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile)
var Writer io.Writer = os.Stderr // io.Writer to write warnings to. Defaults to os.Stderr.
// exitFn - This variable allows to test os.Exit calls
var exitFn = os.Exit
// completionWriter - Writer where the completion results will be written to.
// Set as a variable to allow for easy testing.
var completionWriter io.Writer = os.Stdout
// GetOpt - main object.
type GetOpt struct {
// This is the main tree structure that gets build during the option and command definition
programTree *programTree
// This is the node that gets selected after parsing the CLI args.
//
// NOTE: When calling dispatch the programTree above is overwritten to be finalNode.
// This finalNode shouldn't be used downstream.
finalNode *programTree
}
// Mode - Operation mode for short options
type Mode int
// Operation modes
const (
Normal Mode = iota
Bundling
SingleDash
)
// UnknownMode - Unknown option mode
type UnknownMode int
// Unknown option modes - Action taken when an unknown option is encountered.
const (
Fail UnknownMode = iota
Warn
Pass
)
// CommandFn - Function signature for commands
type CommandFn func(context.Context, *GetOpt, []string) error
// New returns an empty object of type GetOpt.
// This is the starting point when using go-getoptions.
// For example:
//
// opt := getoptions.New()
func New() *GetOpt {
gopt := &GetOpt{}
gopt.programTree = &programTree{
Name: filepath.Base(os.Args[0]),
ChildCommands: map[string]*programTree{},
ChildOptions: map[string]*option.Option{},
Level: 0,
}
return gopt
}
// TODO: Get rid of self and instead have a NewDetailed(name, description)
// Self - Set a custom name and description that will show in the automated help.
// If name is an empty string, it will only use the description and use the name as the executable name.
func (gopt *GetOpt) Self(name string, description string) *GetOpt {
// TODO: Should this only be allowed at the root node level
if name == "" {
name = filepath.Base(os.Args[0])
}
gopt.programTree.Name = name
gopt.programTree.Description = description
return gopt
}
// SetMode - Sets the Operation Mode.
// The operation mode only affects options starting with a single dash '-'.
// The available operation modes are: normal, bundling or singleDash.
//
// The following table shows the different operation modes given the string "-opt=arg".
//
// .Operation Modes for string "-opt=arg"
// |===
// |Mode |Description
//
// |normal |option: opt
// argument: arg
//
// |bundling |option: o
// argument: nil
// option: p
// argument: nil
// option: t
// argument: arg
//
// |singleDash |option: o
// argument: pt=arg
//
// |===
//
// See https://github.com/DavidGamba/go-getoptions#operation_modes for more details.
func (gopt *GetOpt) SetMode(mode Mode) *GetOpt {
gopt.programTree.mode = mode
return gopt
}
// SetUnknownMode - Determines how to behave when encountering an unknown option.
//
// • 'fail' (default) will make 'Parse' return an error with the unknown option information.
//
// • 'warn' will make 'Parse' print a user warning indicating there was an unknown option.
// The unknown option will be left in the remaining array.
//
// • 'pass' will make 'Parse' ignore any unknown options and they will be passed onto the 'remaining' slice.
// This allows for subcommands.
// TODO: Add aliases
func (gopt *GetOpt) SetUnknownMode(mode UnknownMode) *GetOpt {
gopt.programTree.unknownMode = mode
return gopt
}
// SetRequireOrder - Stop parsing options when an unknown entry is found.
// Put every remaining argument, including the unknown entry, in the `remaining` slice.
//
// This is helpful when doing wrappers to other tools and you want to pass all options and arguments to that wrapper without relying on '--'.
//
// An unknown entry is assumed to be the first argument that is not a known option or an argument to an option.
// When a subcommand is found, stop parsing arguments and let a subcommand handler handle the remaining arguments.
// For example:
//
// program --opt arg unknown-command --subopt subarg
//
// In the example above, `--opt` is an option and `arg` is an argument to an option, making `unknown-command` the first non option argument.
//
// This method is useful when both the program and the unknown-command have option handlers for the same option.
//
// For example, with:
//
// program --help
//
// `--help` is handled by `program`, and with:
//
// program unknown-command --help
//
// `--help` is not handled by `program` since there was a unknown-command that caused the parsing to stop.
// In this case, the `remaining` slice will contain `['unknown-command', '--help']` and that can be send to the wrapper handling code.
//
// NOTE: In cases when the wrapper is written as a command use `opt.UnsetOptions` instead.
func (gopt *GetOpt) SetRequireOrder() *GetOpt {
gopt.programTree.requireOrder = true
return gopt
}
// CustomCompletion - Add a custom completion list.
func (gopt *GetOpt) CustomCompletion(list ...string) *GetOpt {
gopt.programTree.Suggestions = list
return gopt
}
// UnsetOptions - Unsets inherited options from parent program and parent commands.
// This is useful when writing wrappers around other commands.
//
// NOTE: Use in combination with `opt.SetUnknownMode(getoptions.Pass)`
func (gopt *GetOpt) UnsetOptions() *GetOpt {
gopt.programTree.ChildOptions = map[string]*option.Option{}
gopt.programTree.skipOptionsCopy = true
return gopt
}
// HelpSynopsisArg - Defines the help synopsis args description.
// Defaults to: [<args>]
func (gopt *GetOpt) HelpSynopsisArg(arg, description string) *GetOpt {
gopt.programTree.SynopsisArgs = append(gopt.programTree.SynopsisArgs, help.SynopsisArg{Arg: arg, Description: description})
return gopt
}
// NewCommand - Returns a new GetOpt object representing a new command.
//
// NOTE: commands must be declared after all options are declared.
func (gopt *GetOpt) NewCommand(name string, description string) *GetOpt {
cmd := &GetOpt{}
command := &programTree{
Name: name,
Description: description,
HelpCommandName: gopt.programTree.HelpCommandName,
ChildCommands: map[string]*programTree{},
ChildOptions: map[string]*option.Option{},
Parent: gopt.programTree,
Level: gopt.programTree.Level + 1,
mapKeysToLower: gopt.programTree.mapKeysToLower,
unknownMode: gopt.programTree.unknownMode,
requireOrder: gopt.programTree.requireOrder,
}
// TODO: Copying options from parent to child can't be done on declaration
// because if an option is declared after the command then it is not part of
// the tree.
// However, the other side of the coin, is that if we do it in the parse call
// then I have to wait until parse to find duplicates and panics.
// // Copy option definitions from parent to child
// for k, v := range gopt.programTree.ChildOptions {
// // The option parent doesn't match properly here.
// // I should in a way create a copy of the option but I still want a pointer to the data.
//
// // c := v.Copy() // copy that maintains a pointer to the underlying data
// // c.SetParent(command)
//
// // TODO: This is doing an overwrite, ensure it doesn't exist
// // command.ChildOptions[k] = c
// command.ChildOptions[k] = v
// }
cmd.programTree = command
gopt.programTree.AddChildCommand(name, command)
copyOptionsFromParent(gopt.programTree)
return cmd
}
func copyOptionsFromParent(parent *programTree) {
for k, v := range parent.ChildOptions {
for _, command := range parent.ChildCommands {
// don't copy options to help command
if command.Name == parent.HelpCommandName {
continue
}
if command.skipOptionsCopy {
continue
}
command.ChildOptions[k] = v
}
}
for _, command := range parent.ChildCommands {
copyOptionsFromParent(command)
}
}
// SetCommandFn - Defines the command entry point function.
func (gopt *GetOpt) SetCommandFn(fn CommandFn) *GetOpt {
gopt.programTree.CommandFn = fn
return gopt
}
// Parse - Call the parse method when done describing.
// It will operate on any given slice of strings and return the remaining (non
// used) command line arguments.
// This allows to easily subcommand.
//
// Parsing style is controlled by the `Set` methods (SetMode, SetRequireOrder, etc).
//
// // Declare the GetOptions object
// opt := getoptions.New()
// ...
// // Parse cmdline arguments or any provided []string
// remaining, err := opt.Parse(os.Args[1:])
func (gopt *GetOpt) Parse(args []string) ([]string, error) {
compLine := os.Getenv("COMP_LINE")
if compLine != "" {
completionTarget := "bash"
zsh := os.Getenv("ZSHELL")
if zsh != "" {
completionTarget = "zsh"
}
// COMP_LINE has a single trailing space when the completion isn't complete and 2 when it is
re := regexp.MustCompile(`\s+`)
compLineParts := re.Split(compLine, -1)
// Drop the trailing "" part if the second argument is not "". COMP_LINE alone isn't enough to tell if we are triggering a completion or not.
// Dropping
// $ ./complex message
// 2022/01/01 00:30:23 user.go:296: COMP_LINE: './complex message ', parts: []string{"./complex", "message"}, args: []string{"./complex", "message", "./complex"}
// 2022/01/01 00:30:23 user.go:305: completions: getoptions.completions{"message"}
// Not dropping -> Stuck
// $ ./complex mes
// 2022/01/01 00:32:14 user.go:299: COMP_LINE: './complex mes ', parts: []string{"./complex", "mes", ""}, args: []string{"./complex", "mes", "./complex"}
// 2022/01/01 00:32:14 user.go:308: completions: getoptions.completions{"help", "log", "lswrapper", "message", "show", "slow"}
if len(compLineParts) > 0 && compLineParts[len(compLineParts)-1] == "" && len(args) > 2 && args[1] != "" {
compLineParts = compLineParts[:len(compLineParts)-1]
}
// Only pass an empty arg to parse when we have 2 trailing spaces indicating we are ready for the next completion.
// if !strings.HasSuffix(compLine, " ") && len(compLineParts) > 0 && compLineParts[len(compLineParts)-1] == "" {
// compLineParts = compLineParts[:len(compLineParts)-1]
// }
// In some cases, the first completion only gets one space
// NOTE: Bash completions have = as a special char and results should be trimmed form the = on.
Logger.SetPrefix("\n")
Logger.Printf("mode: %s, COMP_LINE: '%s', parts: %#v, args: %#v\n", completionTarget, compLine, compLineParts, args)
_, completions, err := parseCLIArgs(completionTarget, gopt.programTree, compLineParts, Normal)
if err != nil {
fmt.Fprintf(Writer, "\nERROR: %s\n", err)
exitFn(124) // programmable completion restarts from the beginning, with an attempt to find a new compspec for that command.
// Ignore errors in completion mode
return nil, nil
}
Logger.Printf("completions: %#v\n", completions)
fmt.Fprintln(completionWriter, strings.Join(completions, "\n"))
exitFn(124) // programmable completion restarts from the beginning, with an attempt to find a new compspec for that command.
return nil, nil
}
// WIP:
// After we are done parsing, we know what node in the tree we are.
// I could easily dispatch from here.
// Think about whether or not there is value in dispatching directly from parse or if it is better to call the dispatch function.
// I came up with the conclusion that dispatch provides a bunch of flexibility and explicitness.
// TODO: parseCLIArgs needs to return the remaining array
node, _, err := parseCLIArgs("", gopt.programTree, args, gopt.programTree.mode)
gopt.finalNode = node
if err != nil {
return nil, err
}
// Only validate required options at the parse call when the final node is the parent
// This to enable handling the help option in a command
if gopt.finalNode.Parent == nil {
// Check for help options before checking required.
// If the help is called, don't check for required options since the program wont run.
if gopt.finalNode.HelpCommandName == "" || !gopt.Called(gopt.finalNode.HelpCommandName) {
// Validate required options
for _, option := range node.ChildOptions {
err := option.CheckRequired()
if err != nil {
return nil, fmt.Errorf("%w%s", ErrorParsing, err.Error())
}
}
}
}
for _, option := range node.UnknownOptions {
// Check for unknown mode at the node that we want to validate
switch gopt.finalNode.unknownMode {
case Fail:
return nil, fmt.Errorf(text.MessageOnUnknown, option.Name)
case Warn:
fmt.Fprintf(Writer, text.WarningOnUnknown+"\n", option.Name)
}
}
return node.ChildText, nil
}
// Dispatch - Handles calling commands and subcommands after the call to Parse.
func (gopt *GetOpt) Dispatch(ctx context.Context, remaining []string) error {
if gopt.finalNode.HelpCommandName != "" && gopt.Called(gopt.finalNode.HelpCommandName) {
fmt.Fprint(Writer, helpOutput(gopt.finalNode))
return ErrorHelpCalled
}
// Validate required options
for _, option := range gopt.finalNode.ChildOptions {
err := option.CheckRequired()
if err != nil {
return fmt.Errorf("%w%s", ErrorParsing, err.Error())
}
}
if gopt.finalNode.CommandFn != nil {
return gopt.finalNode.CommandFn(ctx, &GetOpt{gopt.finalNode, gopt.finalNode}, remaining)
}
if gopt.finalNode.Parent != nil {
// landing help for commands without fn that have children
if len(gopt.finalNode.ChildCommands) > 1 {
fmt.Fprint(Writer, helpOutput(gopt.finalNode))
return ErrorHelpCalled
}
// TODO: This should probably panic at the parse call with validation instead of waiting for a runtime error.
return fmt.Errorf("command '%s' has no defined CommandFn", gopt.finalNode.Name)
}
fmt.Fprint(Writer, gopt.Help())
return nil
}