-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathra_cmd.go
More file actions
389 lines (337 loc) · 12.6 KB
/
Copy pathra_cmd.go
File metadata and controls
389 lines (337 loc) · 12.6 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
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
package ra
import (
"fmt"
)
type UsageHeaders struct {
Usage string
Commands string
Arguments string
GlobalOptions string
SubcommandPlaceholder string
}
func DefaultUsageHeaders() UsageHeaders {
return UsageHeaders{
Usage: "Usage:",
Commands: "Commands:",
Arguments: "Arguments:",
GlobalOptions: "Global options:",
SubcommandPlaceholder: "subcommand",
}
}
type ParseHooks struct {
PostParse func(cmd *Cmd, err error) // Called after parsing, before any output
// ErrorHint, if set, is called when ParseOrExit is about to print a user-input
// parse error. A non-empty return is printed on its own line between the error
// message and the usage string, letting callers attach context-aware guidance.
ErrorHint func(cmd *Cmd, err error) string
}
type Cmd struct {
name string
description string
flags map[string]any // flag name -> flag itself (either a Flag[T] or SliceFlag[T])
positional []string // positional flags, i.e. flags that are positional args
nonPositional []string // non-positional flags, i.e. flags that are only named
globalFlags []string // flags that will be applied to all subcommands
overriddenGlobalFlags map[string]any // global flags that were overridden by non-global flags (name collisions)
shadowedShortFlags map[string]bool // global flags that lost their short flag to non-global flags (short collisions)
shadowedNameFlags map[string]bool // global flags that lost their name to non-global flags (name collisions)
subCmds map[string]*Cmd
shortToName map[string]string // short flag -> full name mapping
// completion
completionEnabled bool // if true, __complete subcommand is recognized
// options
customUsage func(bool) // if set, this function will be called to print usage instead of the default
parseHooks *ParseHooks // if set, hooks will be called after parsing
helpEnabled bool // default true automatically adds a help flag
hidden bool // if true, omit this command from help output entirely
hiddenInShortHelp bool // if true, hide from short help (-h), show in long help (--help)
autoHelpOnNoArgs bool // if true, show help when no args provided and required args exist
usageHeaders *UsageHeaders // custom headers for usage output
// state post-parse
used *bool // after parsing, whether this command was invoked
configured map[string]bool // specified flags from flags.
unknownArgs []string // unknown args when ignoreUnknown is true
excessPositionals []string // positionals no flag could consume; reported together after parsing
lastVariadicFlag string // last variadic flag that was used
sawFlag bool // true if we've seen a flag since the last variadic
}
func NewCmd(name string) *Cmd {
c := &Cmd{
name: name,
flags: make(map[string]any),
positional: []string{},
overriddenGlobalFlags: make(map[string]any),
shadowedShortFlags: make(map[string]bool),
shadowedNameFlags: make(map[string]bool),
subCmds: make(map[string]*Cmd),
configured: make(map[string]bool),
helpEnabled: true,
shortToName: make(map[string]string),
}
return c
}
func (c *Cmd) SetDescription(desc string) *Cmd {
c.description = desc
return c
}
func (c *Cmd) SetCustomUsage(fn func(isLongHelp bool)) *Cmd {
c.customUsage = fn
return c
}
func (c *Cmd) SetParseHooks(hooks *ParseHooks) *Cmd {
c.parseHooks = hooks
return c
}
func (c *Cmd) SetHelpEnabled(enable bool) *Cmd {
c.helpEnabled = enable
return c
}
func (c *Cmd) SetHidden(hidden bool) *Cmd {
c.hidden = hidden
return c
}
func (c *Cmd) SetHiddenInShortHelp(hidden bool) *Cmd {
c.hiddenInShortHelp = hidden
return c
}
func (c *Cmd) SetAutoHelpOnNoArgs(enable bool) *Cmd {
c.autoHelpOnNoArgs = enable
return c
}
func (c *Cmd) SetUsageHeaders(headers UsageHeaders) *Cmd {
c.usageHeaders = &headers
return c
}
func (c *Cmd) getUsageHeaders() UsageHeaders {
if c.usageHeaders != nil {
h := *c.usageHeaders
if h.SubcommandPlaceholder == "" {
h.SubcommandPlaceholder = DefaultUsageHeaders().SubcommandPlaceholder
}
return h
}
return DefaultUsageHeaders()
}
func (c *Cmd) applyGlobalFlags(subCmd *Cmd) error {
// Apply global flags
for _, globalFlagName := range c.globalFlags {
var flag any
var exists bool
// Check if we have an overridden version (original with short intact)
if overriddenFlag, overriddenExists := c.overriddenGlobalFlags[globalFlagName]; overriddenExists {
flag = overriddenFlag
exists = true
} else {
flag, exists = c.flags[globalFlagName]
}
if exists {
// Only add flag if it doesn't already exist in subcommand
if _, exists := subCmd.flags[globalFlagName]; !exists {
subCmd.flags[globalFlagName] = flag
if base := getBaseFlag(flag); base != nil && base.Short != "" {
// Only set the short mapping if a command-specific flag hasn't already claimed it
if _, shortTaken := subCmd.shortToName[base.Short]; !shortTaken {
subCmd.shortToName[base.Short] = base.Name
}
}
// Also add to subcommand's global flags list and non-positional list
subCmd.globalFlags = append(subCmd.globalFlags, globalFlagName)
subCmd.nonPositional = append(subCmd.nonPositional, globalFlagName)
}
}
}
return nil
}
// ResetParseState resets all parsing-related state to a clean slate, allowing the command
// to be parsed again from scratch.
//
// ADVANCED: This is for multi-parse scenarios. Most applications should parse once.
// All flag values are reset to defaults - cache any needed values before calling this.
func (c *Cmd) ResetParseState() {
if c.used != nil {
*c.used = false
}
c.configured = make(map[string]bool)
c.unknownArgs = []string{}
c.lastVariadicFlag = ""
c.sawFlag = false
// Reset all flag values to their defaults
_ = c.setDefaults()
// Recursively reset all subcommands
for _, subCmd := range c.subCmds {
if subCmd.used != nil {
*subCmd.used = false
}
subCmd.ResetParseState()
}
}
// Whether a flag was explicitly configured by the user.
func (c *Cmd) Configured(name string) bool {
// Check if flag is configured in this command
if configured, exists := c.configured[name]; exists && configured {
return true
}
// Check all invoked subcommands recursively
for _, subCmd := range c.subCmds {
if subCmd.used != nil && *subCmd.used {
if subCmd.Configured(name) {
return true
}
}
}
return false
}
func (c *Cmd) GetUnknownArgs() []string {
return c.unknownArgs
}
func (c *Cmd) RegisterCmd(subCmd *Cmd) (*bool, error) {
if _, exists := c.subCmds[subCmd.name]; exists {
return nil, fmt.Errorf("command %q already defined", subCmd.name)
}
c.subCmds[subCmd.name] = subCmd
subCmd.used = new(bool)
// Apply global flags to subcommand for usage generation
if err := c.applyGlobalFlags(subCmd); err != nil {
return nil, err
}
return subCmd.used, nil
}
func (c *Cmd) validatePositionalOnlyAfterVariadic(flagName string) error {
// Check if there's already a variadic positional flag
for _, existingName := range c.positional {
existingFlag := c.flags[existingName]
// Check if this existing flag is variadic
switch f := existingFlag.(type) {
case *StringSliceFlag:
if f.Variadic {
return fmt.Errorf("cannot register positional-only flag %q after variadic positional flag %q (positional-only flags cannot be set after variadic flags)", flagName, existingName)
}
case *IntSliceFlag:
if f.Variadic {
return fmt.Errorf("cannot register positional-only flag %q after variadic positional flag %q (positional-only flags cannot be set after variadic flags)", flagName, existingName)
}
case *Int64SliceFlag:
if f.Variadic {
return fmt.Errorf("cannot register positional-only flag %q after variadic positional flag %q (positional-only flags cannot be set after variadic flags)", flagName, existingName)
}
case *Float64SliceFlag:
if f.Variadic {
return fmt.Errorf("cannot register positional-only flag %q after variadic positional flag %q (positional-only flags cannot be set after variadic flags)", flagName, existingName)
}
case *BoolSliceFlag:
if f.Variadic {
return fmt.Errorf("cannot register positional-only flag %q after variadic positional flag %q (positional-only flags cannot be set after variadic flags)", flagName, existingName)
}
}
}
return nil
}
// checkForGlobalFlagOverride checks if a non-global flag can override an existing global flag.
// Returns true if the override is allowed, false if not allowed.
func (c *Cmd) checkForGlobalFlagOverride(flagName string, flagShort string, isGlobal bool) (bool, error) {
// Check for name collision
if existingFlag, exists := c.flags[flagName]; exists {
// Allow non-global flag to override global flag
if !isGlobal {
// Check if existing flag is global
isExistingGlobal := false
for _, globalFlagName := range c.globalFlags {
if globalFlagName == flagName {
isExistingGlobal = true
break
}
}
if isExistingGlobal {
// Check if the non-global flag also conflicts on short
base := getBaseFlag(existingFlag)
hasShortConflict := base != nil && base.Short != "" && flagShort == base.Short
if hasShortConflict {
// Both name and short conflict - let non-global completely override
c.overriddenGlobalFlags[flagName] = existingFlag
// Remove short flag mapping
if base.Short != "" {
delete(c.shortToName, base.Short)
}
// Remove from positional/nonPositional lists
for i, name := range c.positional {
if name == flagName {
c.positional = append(c.positional[:i], c.positional[i+1:]...)
break
}
}
for i, name := range c.nonPositional {
if name == flagName {
c.nonPositional = append(c.nonPositional[:i], c.nonPositional[i+1:]...)
break
}
}
return true, nil
} else {
// Only name conflicts - apply name shadowing logic
c.overriddenGlobalFlags[flagName] = existingFlag
c.shadowedNameFlags[flagName] = true
// Store the global flag under a special key so it can still be parsed
if base != nil && base.Short != "" {
// Update shortToName to point to the short key for the global flag
c.shortToName[base.Short] = base.Short // "-v" maps to "v"
// Store global flag under its short name
c.flags[base.Short] = existingFlag
}
// The non-global flag will be stored under the original name "verbose"
// Remove the global flag from positional/nonPositional lists
for i, name := range c.positional {
if name == flagName {
c.positional = append(c.positional[:i], c.positional[i+1:]...)
break
}
}
for i, name := range c.nonPositional {
if name == flagName {
c.nonPositional = append(c.nonPositional[:i], c.nonPositional[i+1:]...)
break
}
}
return true, nil
}
} else {
return false, fmt.Errorf("flag %q already defined", flagName)
}
} else {
return false, fmt.Errorf("flag %q already defined", flagName)
}
}
// Check for short flag collision if we have a short flag
if flagShort != "" && !isGlobal {
if existingFlagName, exists := c.shortToName[flagShort]; exists {
// Check if the existing flag with this short is global
isExistingGlobal := false
for _, globalFlagName := range c.globalFlags {
if globalFlagName == existingFlagName {
isExistingGlobal = true
break
}
}
if isExistingGlobal {
// For short flag collisions, we need to keep the global flag as global
// but remove its short flag from the parent command
// Store the original global flag (with short) for subcommands
existingFlag := c.flags[existingFlagName]
c.overriddenGlobalFlags[existingFlagName] = existingFlag
// Track that this global flag had its short shadowed
c.shadowedShortFlags[existingFlagName] = true
// Remove the short flag mapping - global flag will only be available by full name
delete(c.shortToName, flagShort)
// Create a copy of the global flag without the short for the parent command
flagCopy := deepCopyFlag(existingFlag)
if base := getBaseFlag(flagCopy); base != nil {
base.Short = "" // Remove short flag from the parent command's copy
}
c.flags[existingFlagName] = flagCopy
return true, nil
} else {
return false, fmt.Errorf("short flag %q already defined", flagShort)
}
}
}
return false, nil
}