-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathra_cmd_parse.go
More file actions
2690 lines (2504 loc) · 75 KB
/
Copy pathra_cmd_parse.go
File metadata and controls
2690 lines (2504 loc) · 75 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
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package ra
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/amterp/color"
)
// HelpInvokedErr is returned by ParseOrError when help is invoked (via -h, --help, or auto-help).
// Users can compare against this constant to detect when help was shown instead of a parsing error.
var HelpInvokedErr = errors.New("help invoked")
// DumpInvokedErr is returned by ParseOrError when dump is invoked (via WithDump(true)).
// Users can compare against this constant to detect when dump was shown instead of parsing.
var DumpInvokedErr = errors.New("dump invoked")
// Internal error wrapper to carry exit code for ParseOrExit
type helpInvokedError struct {
output string // The usage text that was/would be output (empty if not yet generated)
exitCode int // The exit code (0 for help, 1 for error)
useStdout bool // Whether to output to stdout (true for help requests) or stderr (false for errors)
isLongHelp bool // true for --help, false for -h or auto-help
isAutoHelp bool // true if triggered by auto-help (no args with required flags)
useCustomUsage bool // true if custom usage function should be used
cmd *Cmd // The command that help was invoked for (for subcommand help)
}
// newHelpInvokedError creates a new helpInvokedError with mandatory cmd field.
// This ensures that help errors always know which command they originated from,
// allowing proper help generation for subcommands.
func newHelpInvokedError(
cmd *Cmd,
exitCode int,
useStdout bool,
isLongHelp bool,
isAutoHelp bool,
useCustomUsage bool,
) *helpInvokedError {
if cmd == nil {
panic("helpInvokedError requires non-nil cmd - this is a programming error")
}
return &helpInvokedError{
output: "",
exitCode: exitCode,
useStdout: useStdout,
isLongHelp: isLongHelp,
isAutoHelp: isAutoHelp,
useCustomUsage: useCustomUsage,
cmd: cmd,
}
}
func (e *helpInvokedError) Error() string {
return HelpInvokedErr.Error()
}
func (e *helpInvokedError) Unwrap() error {
return HelpInvokedErr
}
// Internal error wrapper for dump invocation
type dumpInvokedError struct {
output string // The dump output (empty if not yet generated)
exitCode int // The exit code (0 for successful dump)
}
func (e *dumpInvokedError) Error() string {
return DumpInvokedErr.Error()
}
func (e *dumpInvokedError) Unwrap() error {
return DumpInvokedErr
}
// TooManyPositionalArgsError is returned when positional arguments are provided
// beyond what the command's positional flags can consume. Unused contains every
// unconsumed positional, in the order they appeared.
type TooManyPositionalArgsError struct {
Unused []string
}
func (e *TooManyPositionalArgsError) Error() string {
return fmt.Sprintf("Too many positional arguments. Unused: [%s]", strings.Join(e.Unused, ", "))
}
// ProgrammingError wraps errors caused by incorrect library setup/configuration.
// These are bugs in the code using Ra, not user input errors.
type ProgrammingError struct {
msg string
}
func (e *ProgrammingError) Error() string {
return e.msg
}
// NewProgrammingError creates a new programming error
func NewProgrammingError(msg string) *ProgrammingError {
return &ProgrammingError{msg: msg}
}
func (c *Cmd) ParseOrExit(args []string, opts ...ParseOpt) {
err := c.parse(args, opts...)
// Call PostParse hook after parsing, before any output (success or error)
if c.parseHooks != nil && c.parseHooks.PostParse != nil {
c.parseHooks.PostParse(c, err)
}
if err != nil {
// Check if this is a completion invoked error (output already written)
if _, ok := err.(*completionInvokedError); ok {
osExit(0)
return
}
// Check if this is a help invoked error
if helpErr, ok := err.(*helpInvokedError); ok {
// Determine which command to generate help for
targetCmd := c
if helpErr.cmd != nil {
targetCmd = helpErr.cmd
}
// Generate help output now, after PostParse hook has been called
var output string
if helpErr.useCustomUsage {
if targetCmd.customUsage != nil {
targetCmd.customUsage(helpErr.isLongHelp)
output = "" // Custom usage handles output directly
}
} else {
if helpErr.isLongHelp {
output = targetCmd.GenerateLongUsage()
} else {
output = targetCmd.GenerateShortUsage()
}
}
// Route output to stdout for help requests, stderr for errors
if output != "" {
if helpErr.useStdout {
fmt.Fprint(stdoutWriter, output)
} else {
fmt.Fprint(stderrWriter, output)
}
}
osExit(helpErr.exitCode)
} else if dumpErr, ok := err.(*dumpInvokedError); ok {
// Generate dump output now, after PostParse hook has been called
output := c.generateDump(args, opts...)
if output != "" {
fmt.Fprint(stdoutWriter, output)
}
osExit(dumpErr.exitCode)
} else if _, ok := err.(*ProgrammingError); ok {
// Programming error - show only error message (no usage)
fmt.Fprintln(stderrWriter, err.Error())
osExit(1)
} else {
// Regular error - show error message and usage
fmt.Fprintln(stderrWriter, err.Error())
if c.parseHooks != nil && c.parseHooks.ErrorHint != nil {
if hint := c.parseHooks.ErrorHint(c, err); hint != "" {
fmt.Fprintln(stderrWriter, hint)
}
}
fmt.Fprintln(stderrWriter)
fmt.Fprint(stderrWriter, c.GenerateLongUsage())
osExit(1)
}
}
}
func (c *Cmd) ParseOrError(args []string, opts ...ParseOpt) error {
err := c.parse(args, opts...)
// Call PostParse hook after parsing, before any output (success or error)
if c.parseHooks != nil && c.parseHooks.PostParse != nil {
c.parseHooks.PostParse(c, err)
}
if err != nil {
// Check if this is a completion invoked error
if _, ok := err.(*completionInvokedError); ok {
return CompletionInvokedErr
}
// Check if this is a help invoked error
if helpErr, ok := err.(*helpInvokedError); ok {
// Determine which command to use for custom usage
targetCmd := c
if helpErr.cmd != nil {
targetCmd = helpErr.cmd
}
// Call custom usage function if it exists, even though ParseOrError doesn't display output
// This maintains backward compatibility for custom usage functions that may have side effects
if helpErr.useCustomUsage && targetCmd.customUsage != nil {
targetCmd.customUsage(helpErr.isLongHelp)
}
return HelpInvokedErr
} else if _, ok := err.(*dumpInvokedError); ok {
// Dump was invoked - ParseOrError doesn't display output, but we return the standard error
return DumpInvokedErr
}
}
return err
}
func (c *Cmd) parse(args []string, opts ...ParseOpt) error {
return c.parseWithPreserveState(args, false, opts...)
}
func (c *Cmd) parseWithPreserveState(args []string, preserveConfigured bool, opts ...ParseOpt) error {
initializeColorFromEnv()
cfg := &parseCfg{}
for _, opt := range opts {
opt(cfg)
}
// reset state in case this is called multiple times
if !preserveConfigured {
c.configured = make(map[string]bool)
}
c.unknownArgs = []string{}
c.excessPositionals = []string{}
c.collectingSlice = ""
c.sawFlag = false
// Add help flags if enabled
if c.helpEnabled {
if _, exists := c.flags["help"]; !exists {
NewBool(
"help",
).SetShort("h").
SetUsage("Print usage string.").
SetOptional(true).
Register(c, WithGlobal(true))
}
}
if err := c.validateBeforeParsing(); err != nil {
return err
}
// Set defaults first
if err := c.setDefaults(); err != nil {
return err
}
// Check for __complete - divert to completion logic if enabled
if c.completionEnabled && len(args) > 0 && args[0] == "__complete" {
return c.handleCompletion(args[1:])
}
// Check for dump mode - if enabled, generate dump output and return
if cfg.dump {
return &dumpInvokedError{
output: "", // Will be generated later, after PostParse hook
exitCode: 0,
}
}
// Check for auto-help: if enabled, no args provided, and something on this
// invocation would have required an argument. The default sub-command counts:
// routing a bare invocation into it would answer with "missing required
// argument", which hides the fact that other commands exist. Help says both.
if c.autoHelpOnNoArgs && len(args) == 0 && (c.hasRequiredFlags() || c.defaultRequiresArgs()) {
return newHelpInvokedError(
c, // cmd
0, // exitCode
true, // useStdout
false, // isLongHelp (auto-help uses short help)
true, // isAutoHelp
c.customUsage != nil, // useCustomUsage
)
}
// Check if we have number shorts mode
numberShortsMode := c.hasNumberShorts()
// Parse arguments
i := 0
seenDashDash := false // Track if we've seen -- and should treat everything as positional
for i < len(args) {
arg := args[i]
// Check for -- (end of flags marker)
if arg == "--" && !seenDashDash {
seenDashDash = true
i++
continue
}
// If we're in positional-only mode, treat everything as positional
if seenDashDash {
if err := c.assignPositionalWithMode(arg, posAfterDashDash); err != nil {
// `--` suppresses sub-command lookup, so this is how a caller
// says "route to the default even though the value looks like
// a command name". Re-prefix `--` so it keeps meaning that all
// the way down.
if routed, handled := c.routeToDefault(append([]string{"--"}, args[i:]...), opts...); handled {
return routed
}
if cfg.ignoreUnknown {
c.unknownArgs = append(c.unknownArgs, arg)
} else if !c.collectIfExcessPositional(arg, err) {
return err
}
}
i++
continue
}
// Check for subcommand first (only if not in positional-only mode)
if !strings.HasPrefix(arg, "-") {
if subCmd, exists := c.subCmds[arg]; exists {
// The named command wins over the default, which is what makes
// the default's own name a usable escape hatch for a value that
// collides with a sibling's.
return c.dispatchTo(subCmd, args[i+1:], opts...)
}
}
// Handle flags (only if not in positional-only mode)
if strings.HasPrefix(arg, "-") {
consumed, err := c.parseFlag(args, i, numberShortsMode, cfg)
if err != nil {
if err.Error() == "not a flag: "+arg {
// This is a negative number, treat as positional
if err := c.assignPositional(arg); err != nil {
if routed, handled := c.routeToDefault(args[i:], opts...); handled {
return routed
}
if cfg.ignoreUnknown {
c.unknownArgs = append(c.unknownArgs, arg)
} else if !c.collectIfExcessPositional(arg, err) {
return err
}
}
i++
continue
}
// Handle unknown flags
if cfg.variadicUnknownFlags {
// Check if we have an active variadic or an unassigned variadic positional.
// This allows variadics to be "activated" in two ways:
// 1. Already active (collectingSlice != "") from consuming a previous value
// 2. Newly activated by finding the first unassigned variadic positional
// This enables: radd test.rad -U (where -U activates the variadic)
// Swallowing flag-shaped tokens is raw rest capture, which stays
// the variadic's job. A plain list may be the active collector,
// but it declines here so the unknown flag stays the error it is.
variadicFlag := c.collectingSlice
if variadicFlag != "" && !isVariadicFlag(c.flags[variadicFlag]) {
variadicFlag = ""
}
if variadicFlag == "" {
variadicFlag = c.findNextUnassignedVariadicPositional()
}
if variadicFlag != "" {
// We have a variadic that can consume this unknown flag
if err := c.assignPositionalWithMode(arg, posUnknownFlagToken); err != nil {
// If variadic assignment fails, fall back to normal unknown handling
if routed, handled := c.routeToDefault(args[i:], opts...); handled {
return routed
}
if cfg.ignoreUnknown {
c.unknownArgs = append(c.unknownArgs, arg)
} else if !c.collectIfExcessPositional(arg, err) {
return err
}
} else {
// Successfully assigned - activate the variadic if it wasn't already
c.collectingSlice = variadicFlag
}
i++
continue
}
}
// A flag this command has never heard of may still belong to
// the default. That matters because a default's own flags are
// registered on it, not here, so without this only the
// positional spellings of the default would be reachable.
if routed, handled := c.routeToDefault(args[i:], opts...); handled {
return routed
}
if cfg.ignoreUnknown {
c.unknownArgs = append(c.unknownArgs, arg)
i++
continue
}
return err
}
c.sawFlag = true
c.collectingSlice = "" // A flag ends the current collection run
i += consumed
} else {
// Handle positional argument
if err := c.assignPositional(arg); err != nil {
// Nothing here can hold this value. If a default exists, the
// value is its first argument rather than an error.
if routed, handled := c.routeToDefault(args[i:], opts...); handled {
return routed
}
if cfg.ignoreUnknown {
c.unknownArgs = append(c.unknownArgs, arg)
} else if !c.collectIfExcessPositional(arg, err) {
return err
}
}
i++
}
}
// Check for help flags after parsing (only if helpEnabled is true)
if c.helpEnabled {
for _, arg := range args {
if arg == "--help" {
return newHelpInvokedError(
c, // cmd
0, // exitCode
true, // useStdout
true, // isLongHelp
false, // isAutoHelp
c.customUsage != nil, // useCustomUsage
)
}
if arg == "-h" {
return newHelpInvokedError(
c, // cmd
0, // exitCode
true, // useStdout
false, // isLongHelp
false, // isAutoHelp
c.customUsage != nil, // useCustomUsage
)
}
}
}
// Reaching here means no sub-command was named at this level - a named one
// returns from the loop. A default takes the invocation, including the bare
// one, which is what makes `tool <args>` work without naming a command.
// After the help scan above, so `tool -h` still shows this level's help
// rather than the default's.
if routed, handled := c.routeToDefault(nil, opts...); handled {
return routed
}
// Report all unconsumed positionals together, now that flags after them have
// still been parsed and help (above) has had a chance to win.
if len(c.excessPositionals) > 0 {
return &TooManyPositionalArgsError{Unused: c.excessPositionals}
}
// Validate required flags
return c.validateRequired()
}
// dispatchTo hands the rest of the command line to a sub-command. Both the
// named route and the default route go through here: the "was used" flag, the
// global-flag cascade and the configured-state carry-over all have to happen
// identically, and a second copy is where they would quietly diverge.
func (c *Cmd) dispatchTo(subCmd *Cmd, rest []string, opts ...ParseOpt) error {
// Delegating returns the child's result directly, so excess positionals
// collected so far would be silently dropped - report them now instead
// (pre-collect fail-fast behavior).
if len(c.excessPositionals) > 0 {
return &TooManyPositionalArgsError{Unused: c.excessPositionals}
}
*subCmd.used = true
// Apply global flags to subcommand before parsing
if err := c.applyGlobalFlags(subCmd); err != nil {
return err
}
// Apply global configured state before parsing
for _, globalFlagName := range c.globalFlags {
if c.configured[globalFlagName] {
subCmd.configured[globalFlagName] = true
}
}
return subCmd.parseWithPreserveState(rest, true, opts...)
}
// routeToDefault hands args to the default sub-command, if one is set. The
// caller passes the offending token *included* - unlike a named dispatch, the
// token is not a command name being consumed, it is the default's first value.
//
// Returns handled=false when there is no default, leaving the caller to report
// its original error.
func (c *Cmd) routeToDefault(rest []string, opts ...ParseOpt) (error, bool) {
def := c.defaultCmd()
if def == nil {
return nil, false
}
return c.dispatchTo(def, rest, opts...), true
}
// collectIfExcessPositional records value for end-of-parse reporting if err is a
// TooManyPositionalArgsError, letting parsing continue past stray positionals so
// the full set can be reported at once. Returns false for any other error, which
// should abort parsing as before (e.g. type-conversion failures).
func (c *Cmd) collectIfExcessPositional(value string, err error) bool {
var tooMany *TooManyPositionalArgsError
if errors.As(err, &tooMany) {
c.excessPositionals = append(c.excessPositionals, value)
return true
}
return false
}
func (c *Cmd) setDefaults() error {
for _, flag := range c.flags {
switch f := flag.(type) {
case *BoolFlag:
if f.Default != nil && !c.configured[f.Name] {
*f.Value = *f.Default
}
case *StringFlag:
if f.Default != nil && !c.configured[f.Name] {
*f.Value = *f.Default
}
case *IntFlag:
if f.Default != nil && !c.configured[f.Name] {
*f.Value = *f.Default
}
case *Int64Flag:
if f.Default != nil && !c.configured[f.Name] {
*f.Value = *f.Default
}
case *Float64Flag:
if f.Default != nil && !c.configured[f.Name] {
*f.Value = *f.Default
}
case *StringSliceFlag:
if !c.configured[f.Name] {
if f.Default != nil {
*f.Value = *f.Default
} else {
*f.Value = []string{}
}
}
case *IntSliceFlag:
if !c.configured[f.Name] {
if f.Default != nil {
*f.Value = *f.Default
} else {
*f.Value = []int{}
}
}
case *Int64SliceFlag:
if !c.configured[f.Name] {
if f.Default != nil {
*f.Value = *f.Default
} else {
*f.Value = []int64{}
}
}
case *Float64SliceFlag:
if !c.configured[f.Name] {
if f.Default != nil {
*f.Value = *f.Default
} else {
*f.Value = []float64{}
}
}
case *BoolSliceFlag:
if !c.configured[f.Name] {
if f.Default != nil {
*f.Value = *f.Default
} else {
*f.Value = []bool{}
}
}
}
}
return nil
}
func (c *Cmd) hasNumberShorts() bool {
for _, flag := range c.flags {
var short string
switch f := flag.(type) {
case *IntFlag:
short = f.Short
case *Int64Flag:
short = f.Short
case *Float64Flag:
short = f.Short
case *StringFlag:
short = f.Short
case *BoolFlag:
short = f.Short
case *StringSliceFlag:
short = f.Short
case *IntSliceFlag:
short = f.Short
case *Int64SliceFlag:
short = f.Short
case *Float64SliceFlag:
short = f.Short
case *BoolSliceFlag:
short = f.Short
}
if short != "" && len(short) == 1 && isDigit(short[0]) {
return true
}
}
return false
}
// findNextUnassignedVariadicPositional returns the name of the first unassigned variadic positional flag
// (in registration order), or an empty string if none exists
func (c *Cmd) findNextUnassignedVariadicPositional() string {
for _, name := range c.positional {
flag := c.flags[name]
// Skip if already configured
if c.configured[name] {
continue
}
// Check if it's a variadic slice flag
switch f := flag.(type) {
case *StringSliceFlag:
if f.Variadic && !f.FlagOnly {
return name
}
case *IntSliceFlag:
if f.Variadic && !f.FlagOnly {
return name
}
case *Int64SliceFlag:
if f.Variadic && !f.FlagOnly {
return name
}
case *Float64SliceFlag:
if f.Variadic && !f.FlagOnly {
return name
}
case *BoolSliceFlag:
if f.Variadic && !f.FlagOnly {
return name
}
}
}
return ""
}
func (c *Cmd) parseFlag(args []string, index int, numberShortsMode bool, cfg *parseCfg) (int, error) {
arg := args[index]
if strings.HasPrefix(arg, "--") {
// Long flag
return c.parseLongFlag(args, index, numberShortsMode, cfg)
} else if strings.HasPrefix(arg, "-") {
// Short flag(s)
return c.parseShortFlag(args, index, numberShortsMode, cfg)
}
return 0, fmt.Errorf("invalid flag: %s", arg)
}
func (c *Cmd) parseLongFlag(args []string, index int, numberShortsMode bool, cfg *parseCfg) (int, error) {
arg := args[index]
flagName := arg[2:] // remove --
// Check for = syntax
var value string
var hasValue bool
if idx := strings.Index(flagName, "="); idx != -1 {
value = flagName[idx+1:]
flagName = flagName[:idx]
hasValue = true
}
flag, exists := c.flags[flagName]
if !exists {
// Before returning unknown flag error, check if help flags are present
if c.helpEnabled && c.hasHelpFlags(args) {
return 0, c.createHelpError(args)
}
return 0, fmt.Errorf("unknown flag: --%s", flagName)
}
c.configured[flagName] = true
switch f := flag.(type) {
case *BoolFlag:
if hasValue {
val, err := c.parseBoolValue(value)
if err != nil {
return 0, fmt.Errorf("invalid value for flag --%s: %s", flagName, err.Error())
}
*f.Value = val
} else {
*f.Value = true
}
return 1, nil
case *StringFlag:
if hasValue {
err := c.setStringValue(f, value)
return 1, err
}
if index+1 >= len(args) {
return 0, fmt.Errorf("flag --%s requires a value", flagName)
}
if err := c.checkFlagValue(args, args[index+1], "--"+flagName, false, numberShortsMode); err != nil {
return 0, err
}
err := c.setStringValue(f, args[index+1])
return 2, err
case *IntFlag:
if hasValue {
err := c.setIntValue(f, value)
return 1, err
}
if index+1 >= len(args) {
return 0, fmt.Errorf("flag --%s requires a value", flagName)
}
if err := c.checkFlagValue(args, args[index+1], "--"+flagName, true, numberShortsMode); err != nil {
return 0, err
}
err := c.setIntValue(f, args[index+1])
return 2, err
case *Int64Flag:
if hasValue {
err := c.setInt64Value(f, value)
return 1, err
}
if index+1 >= len(args) {
return 0, fmt.Errorf("flag --%s requires a value", flagName)
}
if err := c.checkFlagValue(args, args[index+1], "--"+flagName, true, numberShortsMode); err != nil {
return 0, err
}
err := c.setInt64Value(f, args[index+1])
return 2, err
case *Float64Flag:
if hasValue {
err := c.setFloat64Value(f, value)
return 1, err
}
if index+1 >= len(args) {
return 0, fmt.Errorf("flag --%s requires a value", flagName)
}
if err := c.checkFlagValue(args, args[index+1], "--"+flagName, true, numberShortsMode); err != nil {
return 0, err
}
err := c.setFloat64Value(f, args[index+1])
return 2, err
case *StringSliceFlag:
if hasValue {
_, err := c.appendStringSliceValue(f, value)
return 1, err
}
return c.parseSliceFlag(args, index, f, "--"+flagName, numberShortsMode, cfg)
case *IntSliceFlag:
if hasValue {
_, err := c.appendIntSliceValue(f, value)
return 1, err
}
return c.parseIntSliceFlag(args, index, f, "--"+flagName, numberShortsMode)
case *Int64SliceFlag:
if hasValue {
_, err := c.appendInt64SliceValue(f, value)
return 1, err
}
return c.parseInt64SliceFlag(args, index, f, "--"+flagName, numberShortsMode)
case *Float64SliceFlag:
if hasValue {
_, err := c.appendFloat64SliceValue(f, value)
return 1, err
}
return c.parseFloat64SliceFlag(args, index, f, "--"+flagName, numberShortsMode)
case *BoolSliceFlag:
if hasValue {
_, err := c.appendBoolSliceValue(f, value)
return 1, err
}
return c.parseBoolSliceFlag(args, index, f, "--"+flagName, numberShortsMode)
}
return 0, NewProgrammingError(fmt.Sprintf("unsupported flag type for: %s", flagName))
}
func (c *Cmd) parseShortFlag(args []string, index int, numberShortsMode bool, cfg *parseCfg) (int, error) {
arg := args[index]
shorts := arg[1:] // remove -
// Check for = syntax in short flags (e.g., -r=value)
var value string
var hasValue bool
if idx := strings.Index(shorts, "="); idx != -1 {
value = shorts[idx+1:]
shorts = shorts[:idx]
hasValue = true
}
// Check if this is a negative number without number shorts mode
if !numberShortsMode && len(shorts) > 0 && (isDigit(shorts[0]) || shorts[0] == '.') {
// This is a negative number, treat as positional
return 0, fmt.Errorf("not a flag: %s", arg)
}
// In number shorts mode, check if this is a negative number
if numberShortsMode && len(shorts) > 0 && isDigit(shorts[0]) {
// This is a number short flag
if flagName, exists := c.shortToName[shorts]; exists {
flag := c.flags[flagName]
c.configured[flagName] = true
switch f := flag.(type) {
case *IntFlag:
if len(shorts) > 1 {
// Multiple occurrences like -aaa
count := len(shorts)
*f.Value = count
return 1, nil
}
// Single occurrence
if hasValue {
// Use equals value
err := c.setIntValue(f, value)
return 1, err
} else {
// Check if next argument exists and is a valid value. A flag-shaped
// token isn't one, but a negative number is - "-c -5" means -5, the
// same as "--count -5".
if index+1 < len(args) && canBeCountedFlagValue(args[index+1], numberShortsMode) {
// Next arg is a value, try to parse it
err := c.setIntValue(f, args[index+1])
return 2, err
} else {
// No value provided or next arg is a flag, treat as count of 1
*f.Value = 1
return 1, nil
}
}
case *Int64Flag:
if len(shorts) > 1 {
// Multiple occurrences like -nnn
count := len(shorts)
*f.Value = int64(count)
return 1, nil
}
// Single occurrence
if hasValue {
// Use equals value
err := c.setInt64Value(f, value)
return 1, err
} else {
// Check if next argument exists and is a valid value. A flag-shaped
// token isn't one, but a negative number is - "-c -5" means -5, the
// same as "--count -5".
if index+1 < len(args) && canBeCountedFlagValue(args[index+1], numberShortsMode) {
// Next arg is a value, try to parse it
err := c.setInt64Value(f, args[index+1])
return 2, err
} else {
// No value provided or next arg is a flag, treat as count of 1
*f.Value = 1
return 1, nil
}
}
case *StringFlag:
if hasValue {
// Use equals value
err := c.setStringValue(f, value)
return 1, err
} else {
// Use next argument
if index+1 >= len(args) {
return 0, fmt.Errorf("flag -%s requires a value", shorts)
}
if err := c.checkFlagValue(args, args[index+1], "-"+shorts, false, numberShortsMode); err != nil {
return 0, err
}
err := c.setStringValue(f, args[index+1])
return 2, err
}
}
}
}
// Handle equals syntax for short flags (e.g., -r=value or -fr=value)
if hasValue {
// For single short flag with equals, handle directly
if len(shorts) == 1 {
shortStr := string(shorts[0])
flagName, exists := c.shortToName[shortStr]
if !exists {
return 0, fmt.Errorf("unknown shorthand flag: -%s", shortStr)
}
flag := c.flags[flagName]
c.configured[flagName] = true
switch f := flag.(type) {
case *BoolFlag:
val, err := c.parseBoolValue(value)
if err != nil {
return 0, fmt.Errorf("invalid value for flag -%s: %s", shortStr, err.Error())
}
*f.Value = val
return 1, nil
case *StringFlag:
err := c.setStringValue(f, value)
return 1, err
case *IntFlag:
err := c.setIntValue(f, value)
return 1, err
case *Int64Flag:
err := c.setInt64Value(f, value)
return 1, err
case *Float64Flag:
err := c.setFloat64Value(f, value)
return 1, err
case *StringSliceFlag:
_, err := c.appendStringSliceValue(f, value)
return 1, err
case *IntSliceFlag:
_, err := c.appendIntSliceValue(f, value)
return 1, err
case *Int64SliceFlag:
_, err := c.appendInt64SliceValue(f, value)
return 1, err
case *Float64SliceFlag:
_, err := c.appendFloat64SliceValue(f, value)
return 1, err
case *BoolSliceFlag:
_, err := c.appendBoolSliceValue(f, value)
return 1, err
}
return 0, NewProgrammingError(fmt.Sprintf("unsupported flag type for: %s", flagName))
}
// For clustered flags with equals (e.g., -fr=value), fall through to regular clustering
// but the equals value will be used by the last flag in the cluster
}
// Regular short flag processing
consumed := 1
// Check if all chars are the same (for int flag counting)
if len(shorts) > 1 {
firstChar := shorts[0]
allSame := true
for i := 1; i < len(shorts); i++ {
if shorts[i] != firstChar {
allSame = false
break
}
}
if allSame {
// All chars are the same, check if it's an int or int64 flag
if flagName, exists := c.shortToName[string(firstChar)]; exists {
if flag, exists := c.flags[flagName]; exists {
if intFlag, ok := flag.(*IntFlag); ok {
c.configured[flagName] = true
if hasValue {
// Explicit equals value takes precedence over counting
err := c.setIntValue(intFlag, value)
return 1, err
} else {
// This is an int flag being repeated, set it to the count
*intFlag.Value = len(shorts)
return 1, nil
}
}
if int64Flag, ok := flag.(*Int64Flag); ok {
c.configured[flagName] = true
if hasValue {
// Explicit equals value takes precedence over counting
err := c.setInt64Value(int64Flag, value)
return 1, err
} else {
// This is an int64 flag being repeated, set it to the count
*int64Flag.Value = int64(len(shorts))
return 1, nil
}
}
}
}
}
}
for i, short := range shorts {
shortStr := string(short)
flagName, exists := c.shortToName[shortStr]