1package cli
2
3import (
4	"flag"
5	"strings"
6)
7
8type iterativeParser interface {
9	newFlagSet() (*flag.FlagSet, error)
10	useShortOptionHandling() bool
11}
12
13// To enable short-option handling (e.g., "-it" vs "-i -t") we have to
14// iteratively catch parsing errors. This way we achieve LR parsing without
15// transforming any arguments. Otherwise, there is no way we can discriminate
16// combined short options from common arguments that should be left untouched.
17// Pass `shellComplete` to continue parsing options on failure during shell
18// completion when, the user-supplied options may be incomplete.
19func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComplete bool) error {
20	for {
21		err := set.Parse(args)
22		if !ip.useShortOptionHandling() || err == nil {
23			if shellComplete {
24				return nil
25			}
26			return err
27		}
28
29		errStr := err.Error()
30		trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -")
31		if errStr == trimmed {
32			return err
33		}
34
35		// regenerate the initial args with the split short opts
36		argsWereSplit := false
37		for i, arg := range args {
38			// skip args that are not part of the error message
39			if name := strings.TrimLeft(arg, "-"); name != trimmed {
40				continue
41			}
42
43			// if we can't split, the error was accurate
44			shortOpts := splitShortOptions(set, arg)
45			if len(shortOpts) == 1 {
46				return err
47			}
48
49			// swap current argument with the split version
50			args = append(args[:i], append(shortOpts, args[i+1:]...)...)
51			argsWereSplit = true
52			break
53		}
54
55		// This should be an impossible to reach code path, but in case the arg
56		// splitting failed to happen, this will prevent infinite loops
57		if !argsWereSplit {
58			return err
59		}
60
61		// Since custom parsing failed, replace the flag set before retrying
62		newSet, err := ip.newFlagSet()
63		if err != nil {
64			return err
65		}
66		*set = *newSet
67	}
68}
69
70func splitShortOptions(set *flag.FlagSet, arg string) []string {
71	shortFlagsExist := func(s string) bool {
72		for _, c := range s[1:] {
73			if f := set.Lookup(string(c)); f == nil {
74				return false
75			}
76		}
77		return true
78	}
79
80	if !isSplittable(arg) || !shortFlagsExist(arg) {
81		return []string{arg}
82	}
83
84	separated := make([]string, 0, len(arg)-1)
85	for _, flagChar := range arg[1:] {
86		separated = append(separated, "-"+string(flagChar))
87	}
88
89	return separated
90}
91
92func isSplittable(flagArg string) bool {
93	return strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2
94}
95