blob: 8ed61f1db66e1205c0c5380f8c19cef29c5ba46b [file] [log] [blame]
Naveen Sampath04696f72022-06-13 15:19:14 +05301package flags
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "reflect"
8 "sort"
9 "strings"
10 "unicode/utf8"
11)
12
13// Completion is a type containing information of a completion.
14type Completion struct {
15 // The completed item
16 Item string
17
18 // A description of the completed item (optional)
19 Description string
20}
21
22type completions []Completion
23
24func (c completions) Len() int {
25 return len(c)
26}
27
28func (c completions) Less(i, j int) bool {
29 return c[i].Item < c[j].Item
30}
31
32func (c completions) Swap(i, j int) {
33 c[i], c[j] = c[j], c[i]
34}
35
36// Completer is an interface which can be implemented by types
37// to provide custom command line argument completion.
38type Completer interface {
39 // Complete receives a prefix representing a (partial) value
40 // for its type and should provide a list of possible valid
41 // completions.
42 Complete(match string) []Completion
43}
44
45type completion struct {
46 parser *Parser
47}
48
49// Filename is a string alias which provides filename completion.
50type Filename string
51
52func completionsWithoutDescriptions(items []string) []Completion {
53 ret := make([]Completion, len(items))
54
55 for i, v := range items {
56 ret[i].Item = v
57 }
58
59 return ret
60}
61
62// Complete returns a list of existing files with the given
63// prefix.
64func (f *Filename) Complete(match string) []Completion {
65 ret, _ := filepath.Glob(match + "*")
66 if len(ret) == 1 {
67 if info, err := os.Stat(ret[0]); err == nil && info.IsDir() {
68 ret[0] = ret[0] + "/"
69 }
70 }
71 return completionsWithoutDescriptions(ret)
72}
73
74func (c *completion) skipPositional(s *parseState, n int) {
75 if n >= len(s.positional) {
76 s.positional = nil
77 } else {
78 s.positional = s.positional[n:]
79 }
80}
81
82func (c *completion) completeOptionNames(s *parseState, prefix string, match string, short bool) []Completion {
83 if short && len(match) != 0 {
84 return []Completion{
85 {
86 Item: prefix + match,
87 },
88 }
89 }
90
91 var results []Completion
92 repeats := map[string]bool{}
93
94 for name, opt := range s.lookup.longNames {
95 if strings.HasPrefix(name, match) && !opt.Hidden {
96 results = append(results, Completion{
97 Item: defaultLongOptDelimiter + name,
98 Description: opt.Description,
99 })
100
101 if short {
102 repeats[string(opt.ShortName)] = true
103 }
104 }
105 }
106
107 if short {
108 for name, opt := range s.lookup.shortNames {
109 if _, exist := repeats[name]; !exist && strings.HasPrefix(name, match) && !opt.Hidden {
110 results = append(results, Completion{
111 Item: string(defaultShortOptDelimiter) + name,
112 Description: opt.Description,
113 })
114 }
115 }
116 }
117
118 return results
119}
120
121func (c *completion) completeNamesForLongPrefix(s *parseState, prefix string, match string) []Completion {
122 return c.completeOptionNames(s, prefix, match, false)
123}
124
125func (c *completion) completeNamesForShortPrefix(s *parseState, prefix string, match string) []Completion {
126 return c.completeOptionNames(s, prefix, match, true)
127}
128
129func (c *completion) completeCommands(s *parseState, match string) []Completion {
130 n := make([]Completion, 0, len(s.command.commands))
131
132 for _, cmd := range s.command.commands {
133 if cmd.data != c && !cmd.Hidden && strings.HasPrefix(cmd.Name, match) {
134 n = append(n, Completion{
135 Item: cmd.Name,
136 Description: cmd.ShortDescription,
137 })
138 }
139 }
140
141 return n
142}
143
144func (c *completion) completeValue(value reflect.Value, prefix string, match string) []Completion {
145 if value.Kind() == reflect.Slice {
146 value = reflect.New(value.Type().Elem())
147 }
148 i := value.Interface()
149
150 var ret []Completion
151
152 if cmp, ok := i.(Completer); ok {
153 ret = cmp.Complete(match)
154 } else if value.CanAddr() {
155 if cmp, ok = value.Addr().Interface().(Completer); ok {
156 ret = cmp.Complete(match)
157 }
158 }
159
160 for i, v := range ret {
161 ret[i].Item = prefix + v.Item
162 }
163
164 return ret
165}
166
167func (c *completion) complete(args []string) []Completion {
168 if len(args) == 0 {
169 args = []string{""}
170 }
171
172 s := &parseState{
173 args: args,
174 }
175
176 c.parser.fillParseState(s)
177
178 var opt *Option
179
180 for len(s.args) > 1 {
181 arg := s.pop()
182
183 if (c.parser.Options&PassDoubleDash) != None && arg == "--" {
184 opt = nil
185 c.skipPositional(s, len(s.args)-1)
186
187 break
188 }
189
190 if argumentIsOption(arg) {
191 prefix, optname, islong := stripOptionPrefix(arg)
192 optname, _, argument := splitOption(prefix, optname, islong)
193
194 if argument == nil {
195 var o *Option
196 canarg := true
197
198 if islong {
199 o = s.lookup.longNames[optname]
200 } else {
201 for i, r := range optname {
202 sname := string(r)
203 o = s.lookup.shortNames[sname]
204
205 if o == nil {
206 break
207 }
208
209 if i == 0 && o.canArgument() && len(optname) != len(sname) {
210 canarg = false
211 break
212 }
213 }
214 }
215
216 if o == nil && (c.parser.Options&PassAfterNonOption) != None {
217 opt = nil
218 c.skipPositional(s, len(s.args)-1)
219
220 break
221 } else if o != nil && o.canArgument() && !o.OptionalArgument && canarg {
222 if len(s.args) > 1 {
223 s.pop()
224 } else {
225 opt = o
226 }
227 }
228 }
229 } else {
230 if len(s.positional) > 0 {
231 if !s.positional[0].isRemaining() {
232 // Don't advance beyond a remaining positional arg (because
233 // it consumes all subsequent args).
234 s.positional = s.positional[1:]
235 }
236 } else if cmd, ok := s.lookup.commands[arg]; ok {
237 cmd.fillParseState(s)
238 }
239
240 opt = nil
241 }
242 }
243
244 lastarg := s.args[len(s.args)-1]
245 var ret []Completion
246
247 if opt != nil {
248 // Completion for the argument of 'opt'
249 ret = c.completeValue(opt.value, "", lastarg)
250 } else if argumentStartsOption(lastarg) {
251 // Complete the option
252 prefix, optname, islong := stripOptionPrefix(lastarg)
253 optname, split, argument := splitOption(prefix, optname, islong)
254
255 if argument == nil && !islong {
256 rname, n := utf8.DecodeRuneInString(optname)
257 sname := string(rname)
258
259 if opt := s.lookup.shortNames[sname]; opt != nil && opt.canArgument() {
260 ret = c.completeValue(opt.value, prefix+sname, optname[n:])
261 } else {
262 ret = c.completeNamesForShortPrefix(s, prefix, optname)
263 }
264 } else if argument != nil {
265 if islong {
266 opt = s.lookup.longNames[optname]
267 } else {
268 opt = s.lookup.shortNames[optname]
269 }
270
271 if opt != nil {
272 ret = c.completeValue(opt.value, prefix+optname+split, *argument)
273 }
274 } else if islong {
275 ret = c.completeNamesForLongPrefix(s, prefix, optname)
276 } else {
277 ret = c.completeNamesForShortPrefix(s, prefix, optname)
278 }
279 } else if len(s.positional) > 0 {
280 // Complete for positional argument
281 ret = c.completeValue(s.positional[0].value, "", lastarg)
282 } else if len(s.command.commands) > 0 {
283 // Complete for command
284 ret = c.completeCommands(s, lastarg)
285 }
286
287 sort.Sort(completions(ret))
288 return ret
289}
290
291func (c *completion) print(items []Completion, showDescriptions bool) {
292 if showDescriptions && len(items) > 1 {
293 maxl := 0
294
295 for _, v := range items {
296 if len(v.Item) > maxl {
297 maxl = len(v.Item)
298 }
299 }
300
301 for _, v := range items {
302 fmt.Printf("%s", v.Item)
303
304 if len(v.Description) > 0 {
305 fmt.Printf("%s # %s", strings.Repeat(" ", maxl-len(v.Item)), v.Description)
306 }
307
308 fmt.Printf("\n")
309 }
310 } else {
311 for _, v := range items {
312 fmt.Println(v.Item)
313 }
314 }
315}