-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexecute.go
More file actions
345 lines (286 loc) · 9.29 KB
/
execute.go
File metadata and controls
345 lines (286 loc) · 9.29 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
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
package cmder
import (
"context"
"errors"
"flag"
"fmt"
"os"
"regexp"
"strings"
"github.com/brandon1024/cmder/getopt"
)
// ErrIllegalCommandConfiguration is an error returned when a [Command] provided to [Execute] is illegal.
var ErrIllegalCommandConfiguration = errors.New("cmder: illegal command configuration")
// ErrEnvironmentBindFailure is an error returned when [Execute] failed to update a flag value from environment
// variables (see [WithEnvironmentBinding]).
var ErrEnvironmentBindFailure = errors.New("cmder: failed to update flag from environment variable")
// Execute runs a [Command].
//
// # Execution Lifecycle
//
// When executing a command, Execute will call the [Runnable] Run() routine of your command. If the command also
// implements [Initializer] or [Destroyer], the Initialize() or Destroy() routines will be invoked before
// and after calling Run().
//
// If the command implements [RootCommand] and a subcommand is invoked, Execute will invoke the [Initializer] and
// [Destroyer] routines of parent and child commands:
//
// 1. Root [Initializer] Initialize()
// 2. Child [Initializer] Initialize()
// 3. Child [Runnable] Run()
// 4. Child [Destroyer] Destroy()
// 5. Root [Destroyer] Destroy()
//
// If a command implements [RootCommand] but the first argument passed to the command doesn't match a recognized child
// command Name(), the Run() routine will be executed.
//
// # Error Handling
//
// Whenever a lifecycle routine (Initialize(), Run(), Destroy()) returns a non-nil error, execution is aborted
// immediately and the error is returned at once. For example, returning an error from Run() will prevent execution of
// Destroy() of the current command and any parents.
//
// Execute may return [ErrIllegalCommandConfiguration] if a command is misconfigured.
//
// # Command Contexts
//
// A [context.Context] derived from ctx is passed to all lifecycle routines. The context is cancelled when Execute
// returns. Commands should use this context to manage their resources correctly.
//
// # Execution Options
//
// Execute accepts one or more [ExecuteOption] options. You can provide these options to tweak the behavior of Execute.
//
// # Flag Initialization
//
// If the command also implements [FlagInitializer], InitializeFlags() will be invoked to register additional
// command-line flags. Each command/subcommand is given a unique [flag.FlagSet]. Help flags ('-h', '--help') are
// configured automatically if not defined and will instruct Execute to render command usage.
//
// Execute parses getopt-style (GNU/POSIX) command-line arguments with the help of package [getopt]. To use the standard
// [flag] syntax instead, see [WithNativeFlags]. Flags and arguments cannot be interspersed by default. You can change
// this behavior with [WithInterspersedArgs].
//
// To bind environment variables to flags, see [WithEnvironmentBinding].
//
// # Usage and Help Texts
//
// Unless explicitly overridden by the command, the '-h' flag instructs Execute to render command usage information to
// stdout and return [ErrShowUsage]. The default usage text includes a usage synopsis, subcommands and flags. The
// format of the usage text can be adjusted (see [WithUsageTemplate]). Returning [ErrShowUsage] from a command's
// Initialize or Run routines will also instruct Execute to render usage.
//
// Likewise, the '--help' flag instructs Execute to render extended help usage information to stdout, returning
// [ErrShowHelp]. The format may be adjusted (see [WithHelpTemplate]).
func Execute(ctx context.Context, cmd Command, op ...ExecuteOption) error {
// do some checks
if cmd == nil {
return errors.Join(ErrIllegalCommandConfiguration, errors.New("cmder: command cannot be nil"))
}
// prepare executor options
ops := &ExecuteOptions{
args: os.Args[1:],
usageTemplate: DefaultUsageTemplate,
helpTemplate: DefaultHelpTemplate,
outputWriter: os.Stdout,
}
for _, f := range op {
f(ops)
}
// build a stack of command invocations
stack, err := buildCallStack(cmd, ops)
if err != nil {
return err
}
return execute(ctx, stack, ops)
}
// execute traverses the command stack recursively executing the lifecycle routines at each level.
func execute(ctx context.Context, stack []command, ops *ExecuteOptions) error {
if len(stack) == 0 {
return nil
}
// setup context
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var (
this = stack[0]
err error
)
// run init (if applicable)
if err := this.onInit(ctx, ops); err != nil {
return err
}
// if this is a leaf, run, otherwise recurse
if len(stack) == 1 {
err = this.run(ctx, ops)
} else {
err = execute(ctx, stack[1:], ops)
}
if err != nil {
return err
}
// run destroy (if applicable)
if err := this.onDestroy(ctx, ops); err != nil {
return err
}
return nil
}
// An internal representation of a command or subcommand and it's state before execution.
type command struct {
Command
fs *flag.FlagSet
args []string
showUsage bool
showHelp bool
}
// onInit calls the [Initializer] init routine if present on c.
func (c command) onInit(ctx context.Context, ops *ExecuteOptions) error {
var err error
if c.showUsage {
return errors.Join(ErrShowUsage, usage(c, ops))
}
if c.showHelp {
return errors.Join(ErrShowUsage, help(c, ops))
}
if cmd, ok := c.Command.(Initializer); ok {
err = cmd.Initialize(ctx, c.args)
}
if errors.Is(err, ErrShowUsage) {
return errors.Join(err, usage(c, ops))
}
return err
}
// run calls the [Runnable] run routine of c.
func (c command) run(ctx context.Context, ops *ExecuteOptions) error {
if c.showUsage {
return errors.Join(ErrShowUsage, usage(c, ops))
}
if c.showHelp {
return errors.Join(ErrShowUsage, help(c, ops))
}
err := c.Run(ctx, c.args)
if errors.Is(err, ErrShowUsage) {
return errors.Join(err, usage(c, ops))
}
return err
}
// onDestroy calls the [Destroyer] destroy routine if present on c.
func (c command) onDestroy(ctx context.Context, ops *ExecuteOptions) error {
var err error
if cmd, ok := c.Command.(Destroyer); ok {
err = cmd.Destroy(ctx, c.args)
}
if errors.Is(err, ErrShowUsage) {
return errors.Join(err, usage(c, ops))
}
return err
}
// buildCallStack builds a slice representing the command call stack. The first element in the slice is the root
// command and the last is the leaf command.
func buildCallStack(cmd Command, ops *ExecuteOptions) ([]command, error) {
var stack []command
var (
args = ops.args
err error
)
for cmd != nil {
this := command{
Command: cmd,
fs: flag.NewFlagSet(cmd.Name(), flag.ContinueOnError),
}
if c, ok := cmd.(FlagInitializer); ok {
c.InitializeFlags(this.fs)
}
// add help flags
if this.fs.Lookup("h") == nil {
this.fs.BoolVar(&this.showUsage, "h", false, "show command usage information")
}
if this.fs.Lookup("help") == nil {
this.fs.BoolVar(&this.showHelp, "help", false, "show command help information")
}
// bind environment variables
if ops.bindEnv {
if err := bindEnvironmentFlags(stack, this, ops); err != nil {
return nil, err
}
}
this.args, err = parseArgs(this, args, ops)
if err != nil {
return nil, err
}
args = this.args
if len(args) == 0 {
// if no subcommand name given, stop here
cmd = nil
} else if sub, ok := collectSubcommands(cmd)[args[0]]; ok {
// if subcommand name given, continue
args = args[1:]
cmd = sub
} else {
// if arg given but it's not a subcommand name, stop here
cmd = nil
}
stack = append(stack, this)
}
return stack, nil
}
// parseArgs processes args for the given command, returning the unparsed (remaining) arguments.
func parseArgs(cmd command, args []string, ops *ExecuteOptions) ([]string, error) {
var fp flagParser = &getopt.PosixFlagSet{FlagSet: cmd.fs, RelaxedParsing: ops.relaxedFlags}
if ops.nativeFlags {
fp = cmd.fs
}
// interspersed args only possible for leaf commands
interspersed := ops.interspersed
if len(collectSubcommands(cmd.Command)) > 0 {
interspersed = false
}
var processed []string
for len(args) > 0 {
if err := fp.Parse(args); err != nil {
return nil, err
}
args = fp.Args()
if !interspersed {
return args, nil
}
if len(args) > 0 {
processed = append(processed, args[0])
args = args[1:]
}
}
return processed, nil
}
// bindEnvironmentFlags sets flag values from matching environment variables.
func bindEnvironmentFlags(stack []command, cmd command, ops *ExecuteOptions) error {
var components []string
for _, c := range stack {
components = append(components, c.Name())
}
components = append(components, cmd.Name())
var flags []*flag.Flag
cmd.fs.VisitAll(func(f *flag.Flag) {
flags = append(flags, f)
})
for _, flag := range flags {
variable := ops.bindEnvPrefix + formatEnvvar(append(components, flag.Name))
if value, ok := os.LookupEnv(variable); ok {
if err := flag.Value.Set(value); err != nil {
return errors.Join(
ErrEnvironmentBindFailure,
fmt.Errorf("cmder: failed to set flag %s from variable %s", flag.Name, variable),
err,
)
}
}
}
return nil
}
// formatEnvvar generates an environment variable name which maps to the given flag path.
func formatEnvvar(flagPath []string) string {
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
for i, v := range flagPath {
flagPath[i] = strings.ToUpper(reg.ReplaceAllString(v, ""))
}
return strings.Join(flagPath, "_")
}