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