OILS / builtin / completion_osh.py View on Github | oilshell.org

514 lines, 296 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3
4from _devbuild.gen import arg_types
5from _devbuild.gen.syntax_asdl import loc
6from _devbuild.gen.value_asdl import (value, value_e)
7
8from core import completion
9from core import error
10from core import state
11from display import ui
12from core import vm
13from mycpp import mylib
14from mycpp.mylib import log, print_stderr
15from frontend import flag_util
16from frontend import args
17from frontend import consts
18
19_ = log
20
21from typing import Dict, List, Iterator, cast, TYPE_CHECKING
22if TYPE_CHECKING:
23 from _devbuild.gen.runtime_asdl import cmd_value
24 from core.completion import Lookup, OptionState, Api, UserSpec
25 from display import ui
26 from frontend.args import _Attributes
27 from frontend.parse_lib import ParseContext
28 from osh.cmd_eval import CommandEvaluator
29 from osh.split import SplitContext
30 from osh.word_eval import NormalWordEvaluator
31
32
33class _FixedWordsAction(completion.CompletionAction):
34
35 def __init__(self, d):
36 # type: (List[str]) -> None
37 self.d = d
38
39 def Matches(self, comp):
40 # type: (Api) -> Iterator[str]
41 for name in sorted(self.d):
42 if name.startswith(comp.to_complete):
43 yield name
44
45 def Print(self, f):
46 # type: (mylib.BufWriter) -> None
47 f.write('FixedWordsAction ')
48
49
50class _DynamicProcDictAction(completion.CompletionAction):
51 """For completing from proc and aliases dicts, which are mutable.
52
53 Note: this is the same as _FixedWordsAction now, but won't be when the code
54 is statically typed!
55 """
56
57 def __init__(self, d):
58 # type: (state.Procs) -> None
59 self.d = d
60
61 def Matches(self, comp):
62 # type: (Api) -> Iterator[str]
63 for name in self.d.GetNames():
64 if name.startswith(comp.to_complete):
65 yield name
66
67 def Print(self, f):
68 # type: (mylib.BufWriter) -> None
69 f.write('DynamicProcDictAction ')
70
71
72class _DynamicStrDictAction(completion.CompletionAction):
73 """For completing from proc and aliases dicts, which are mutable.
74
75 Note: this is the same as _FixedWordsAction now, but won't be when the code
76 is statically typed!
77 """
78
79 def __init__(self, d):
80 # type: (Dict[str, str]) -> None
81 self.d = d
82
83 def Matches(self, comp):
84 # type: (Api) -> Iterator[str]
85 for name in sorted(self.d):
86 if name.startswith(comp.to_complete):
87 yield name
88
89 def Print(self, f):
90 # type: (mylib.BufWriter) -> None
91 f.write('DynamicStrDictAction ')
92
93
94class SpecBuilder(object):
95
96 def __init__(
97 self,
98 cmd_ev, # type: CommandEvaluator
99 parse_ctx, # type: ParseContext
100 word_ev, # type: NormalWordEvaluator
101 splitter, # type: SplitContext
102 comp_lookup, # type: Lookup
103 help_data, # type: Dict[str, str]
104 errfmt # type: ui.ErrorFormatter
105 ):
106 # type: (...) -> None
107 """
108 Args:
109 cmd_ev: CommandEvaluator for compgen -F
110 parse_ctx, word_ev, splitter: for compgen -W
111 """
112 self.cmd_ev = cmd_ev
113 self.parse_ctx = parse_ctx
114 self.word_ev = word_ev
115 self.splitter = splitter
116 self.comp_lookup = comp_lookup
117
118 self.help_data = help_data
119 # lazily initialized
120 self.topic_list = None # type: List[str]
121
122 self.errfmt = errfmt
123
124 def Build(self, argv, attrs, base_opts):
125 # type: (List[str], _Attributes, Dict[str, bool]) -> UserSpec
126 """Given flags to complete/compgen, return a UserSpec.
127
128 Args:
129 argv: only used for error message
130 """
131 cmd_ev = self.cmd_ev
132
133 # arg_types.compgen is a subset of arg_types.complete (the two users of this
134 # function), so we use the generate type for compgen here.
135 arg = arg_types.compgen(attrs.attrs)
136 actions = [] # type: List[completion.CompletionAction]
137
138 # NOTE: bash doesn't actually check the name until completion time, but
139 # obviously it's better to check here.
140 if arg.F is not None:
141 func_name = arg.F
142 func = cmd_ev.procs.Get(func_name)
143 if func is None:
144 raise error.Usage('function %r not found' % func_name,
145 loc.Missing)
146 actions.append(
147 completion.ShellFuncAction(cmd_ev, func, self.comp_lookup))
148
149 if arg.C is not None:
150 # this can be a shell FUNCTION too, not just an external command
151 # Honestly seems better than -F? Does it also get COMP_CWORD?
152 command = arg.C
153 actions.append(completion.CommandAction(cmd_ev, command))
154 print_stderr('osh warning: complete -C not implemented')
155
156 # NOTE: We need completion for -A action itself!!! bash seems to have it.
157 for name in attrs.actions:
158 if name == 'alias':
159 a = _DynamicStrDictAction(
160 self.parse_ctx.aliases
161 ) # type: completion.CompletionAction
162
163 elif name == 'binding':
164 # TODO: Where do we get this from?
165 a = _FixedWordsAction(['vi-delete'])
166
167 elif name == 'builtin':
168 a = _FixedWordsAction(consts.BUILTIN_NAMES)
169
170 elif name == 'command':
171 # compgen -A command in bash is SIX things: aliases, builtins,
172 # functions, keywords, external commands relative to the current
173 # directory, and external commands in $PATH.
174
175 actions.append(_FixedWordsAction(consts.BUILTIN_NAMES))
176 actions.append(_DynamicStrDictAction(self.parse_ctx.aliases))
177 actions.append(_DynamicProcDictAction(cmd_ev.procs))
178 actions.append(_FixedWordsAction(consts.OSH_KEYWORD_NAMES))
179 actions.append(completion.FileSystemAction(False, True, False))
180
181 # Look on the file system.
182 a = completion.ExternalCommandAction(cmd_ev.mem)
183
184 elif name == 'directory':
185 a = completion.FileSystemAction(True, False, False)
186
187 elif name == 'export':
188 a = completion.ExportedVarsAction(cmd_ev.mem)
189
190 elif name == 'file':
191 a = completion.FileSystemAction(False, False, False)
192
193 elif name == 'function':
194 a = _DynamicProcDictAction(cmd_ev.procs)
195
196 elif name == 'job':
197 a = _FixedWordsAction(['jobs-not-implemented'])
198
199 elif name == 'keyword':
200 a = _FixedWordsAction(consts.OSH_KEYWORD_NAMES)
201
202 elif name == 'user':
203 a = completion.UsersAction()
204
205 elif name == 'variable':
206 a = completion.VariablesAction(cmd_ev.mem)
207
208 elif name == 'helptopic':
209 # Lazy initialization
210 if self.topic_list is None:
211 self.topic_list = self.help_data.keys()
212 a = _FixedWordsAction(self.topic_list)
213
214 elif name == 'setopt':
215 a = _FixedWordsAction(consts.SET_OPTION_NAMES)
216
217 elif name == 'shopt':
218 a = _FixedWordsAction(consts.SHOPT_OPTION_NAMES)
219
220 elif name == 'signal':
221 a = _FixedWordsAction(['TODO:signals'])
222
223 elif name == 'stopped':
224 a = _FixedWordsAction(['jobs-not-implemented'])
225
226 else:
227 raise AssertionError(name)
228
229 actions.append(a)
230
231 # e.g. -W comes after -A directory
232 if arg.W is not None: # could be ''
233 # NOTES:
234 # - Parsing is done at REGISTRATION time, but execution and splitting is
235 # done at COMPLETION time (when the user hits tab). So parse errors
236 # happen early.
237 w_parser = self.parse_ctx.MakeWordParserForPlugin(arg.W)
238
239 try:
240 arg_word = w_parser.ReadForPlugin()
241 except error.Parse as e:
242 self.errfmt.PrettyPrintError(e)
243 raise # Let 'complete' or 'compgen' return 2
244
245 a = completion.DynamicWordsAction(self.word_ev, self.splitter,
246 arg_word, self.errfmt)
247 actions.append(a)
248
249 extra_actions = [] # type: List[completion.CompletionAction]
250 if base_opts.get('plusdirs', False):
251 extra_actions.append(
252 completion.FileSystemAction(True, False, False))
253
254 # These only happen if there were zero shown.
255 else_actions = [] # type: List[completion.CompletionAction]
256 if base_opts.get('default', False):
257 else_actions.append(
258 completion.FileSystemAction(False, False, False))
259 if base_opts.get('dirnames', False):
260 else_actions.append(completion.FileSystemAction(
261 True, False, False))
262
263 if len(actions) == 0 and len(else_actions) == 0:
264 raise error.Usage(
265 'No actions defined in completion: %s' % ' '.join(argv),
266 loc.Missing)
267
268 p = completion.DefaultPredicate() # type: completion._Predicate
269 if arg.X is not None:
270 filter_pat = arg.X
271 if filter_pat.startswith('!'):
272 p = completion.GlobPredicate(False, filter_pat[1:])
273 else:
274 p = completion.GlobPredicate(True, filter_pat)
275
276 # mycpp: rewrite of or
277 prefix = arg.P
278 if prefix is None:
279 prefix = ''
280
281 # mycpp: rewrite of or
282 suffix = arg.S
283 if suffix is None:
284 suffix = ''
285
286 return completion.UserSpec(actions, extra_actions, else_actions, p,
287 prefix, suffix)
288
289
290class Complete(vm._Builtin):
291 """complete builtin - register a completion function.
292
293 NOTE: It's has an CommandEvaluator because it creates a ShellFuncAction, which
294 needs an CommandEvaluator.
295 """
296
297 def __init__(self, spec_builder, comp_lookup):
298 # type: (SpecBuilder, Lookup) -> None
299 self.spec_builder = spec_builder
300 self.comp_lookup = comp_lookup
301
302 def Run(self, cmd_val):
303 # type: (cmd_value.Argv) -> int
304 arg_r = args.Reader(cmd_val.argv, cmd_val.arg_locs)
305 arg_r.Next()
306
307 attrs = flag_util.ParseMore('complete', arg_r)
308 arg = arg_types.complete(attrs.attrs)
309 # TODO: process arg.opt_changes
310
311 commands = arg_r.Rest()
312
313 if arg.D:
314 # if the command doesn't match anything
315 commands.append('__fallback')
316 if arg.E:
317 commands.append('__first') # empty line
318
319 if len(commands) == 0:
320 if len(cmd_val.argv) == 1: # nothing passed at all
321 assert cmd_val.argv[0] == 'complete'
322
323 self.comp_lookup.PrintSpecs()
324 return 0
325 else:
326 # complete -F f is an error
327 raise error.Usage('expected 1 or more commands', loc.Missing)
328
329 base_opts = dict(attrs.opt_changes)
330 try:
331 user_spec = self.spec_builder.Build(cmd_val.argv, attrs, base_opts)
332 except error.Parse as e:
333 # error printed above
334 return 2
335
336 for command in commands:
337 self.comp_lookup.RegisterName(command, base_opts, user_spec)
338
339 # TODO: Hook this up
340 patterns = [] # type: List[str]
341 for pat in patterns:
342 self.comp_lookup.RegisterGlob(pat, base_opts, user_spec)
343
344 return 0
345
346
347class CompGen(vm._Builtin):
348 """Print completions on stdout."""
349
350 def __init__(self, spec_builder):
351 # type: (SpecBuilder) -> None
352 self.spec_builder = spec_builder
353
354 def Run(self, cmd_val):
355 # type: (cmd_value.Argv) -> int
356 arg_r = args.Reader(cmd_val.argv, cmd_val.arg_locs)
357 arg_r.Next()
358
359 arg = flag_util.ParseMore('compgen', arg_r)
360
361 if arg_r.AtEnd():
362 to_complete = ''
363 else:
364 to_complete = arg_r.Peek()
365 arg_r.Next()
366 # bash allows extra arguments here.
367 #if not arg_r.AtEnd():
368 # raise error.Usage('Extra arguments')
369
370 matched = False
371
372 base_opts = dict(arg.opt_changes)
373 try:
374 user_spec = self.spec_builder.Build(cmd_val.argv, arg, base_opts)
375 except error.Parse as e:
376 # error printed above
377 return 2
378
379 # NOTE: Matching bash in passing dummy values for COMP_WORDS and
380 # COMP_CWORD, and also showing ALL COMPREPLY results, not just the ones
381 # that start
382 # with the word to complete.
383 matched = False
384 comp = completion.Api('', 0, 0) # empty string
385 comp.Update('compgen', to_complete, '', -1, None)
386 try:
387 for m, _ in user_spec.AllMatches(comp):
388 matched = True
389 print(m)
390 except error.FatalRuntime:
391 # - DynamicWordsAction: We already printed an error, so return failure.
392 return 1
393
394 # - ShellFuncAction: We do NOT get FatalRuntimeError. We printed an error
395 # in the executor, but RunFuncForCompletion swallows failures. See test
396 # case in builtin-completion.test.sh.
397
398 # TODO:
399 # - need to dedupe results.
400
401 return 0 if matched else 1
402
403
404class CompOpt(vm._Builtin):
405 """Adjust options inside user-defined completion functions."""
406
407 def __init__(self, comp_state, errfmt):
408 # type: (OptionState, ui.ErrorFormatter) -> None
409 self.comp_state = comp_state
410 self.errfmt = errfmt
411
412 def Run(self, cmd_val):
413 # type: (cmd_value.Argv) -> int
414 arg_r = args.Reader(cmd_val.argv, cmd_val.arg_locs)
415 arg_r.Next()
416
417 arg = flag_util.ParseMore('compopt', arg_r)
418
419 if not self.comp_state.currently_completing: # bash also checks this.
420 self.errfmt.Print_(
421 'compopt: not currently executing a completion function')
422 return 1
423
424 self.comp_state.dynamic_opts.update(arg.opt_changes)
425 #log('compopt: %s', arg)
426 #log('compopt %s', base_opts)
427 return 0
428
429
430class CompAdjust(vm._Builtin):
431 """Uses COMP_ARGV and flags produce the 'words' array. Also sets $cur,
432
433 $prev,
434
435 $cword, and $split.
436
437 Note that we do not use COMP_WORDS, which already has splitting applied.
438 bash-completion does a hack to undo or "reassemble" words after erroneous
439 splitting.
440 """
441
442 def __init__(self, mem):
443 # type: (state.Mem) -> None
444 self.mem = mem
445
446 def Run(self, cmd_val):
447 # type: (cmd_value.Argv) -> int
448 arg_r = args.Reader(cmd_val.argv, cmd_val.arg_locs)
449 arg_r.Next()
450
451 attrs = flag_util.ParseMore('compadjust', arg_r)
452 arg = arg_types.compadjust(attrs.attrs)
453 var_names = arg_r.Rest() # Output variables to set
454 for name in var_names:
455 # Ironically we could complete these
456 if name not in ['cur', 'prev', 'words', 'cword']:
457 raise error.Usage('Invalid output variable name %r' % name,
458 loc.Missing)
459 #print(arg)
460
461 # TODO: How does the user test a completion function programmatically? Set
462 # COMP_ARGV?
463 val = self.mem.GetValue('COMP_ARGV')
464 if val.tag() != value_e.BashArray:
465 raise error.Usage("COMP_ARGV should be an array", loc.Missing)
466 comp_argv = cast(value.BashArray, val).strs
467
468 # These are the ones from COMP_WORDBREAKS that we care about. The rest occur
469 # "outside" of words.
470 break_chars = [':', '=']
471 if arg.s: # implied
472 break_chars.remove('=')
473 # NOTE: The syntax is -n := and not -n : -n =.
474 # mycpp: rewrite of or
475 omit_chars = arg.n
476 if omit_chars is None:
477 omit_chars = ''
478
479 for c in omit_chars:
480 if c in break_chars:
481 break_chars.remove(c)
482
483 # argv adjusted according to 'break_chars'.
484 adjusted_argv = [] # type: List[str]
485 for a in comp_argv:
486 completion.AdjustArg(a, break_chars, adjusted_argv)
487
488 if 'words' in var_names:
489 state.BuiltinSetArray(self.mem, 'words', adjusted_argv)
490
491 n = len(adjusted_argv)
492 cur = adjusted_argv[-1]
493 prev = '' if n < 2 else adjusted_argv[-2]
494
495 if arg.s:
496 if cur.startswith('--') and '=' in cur:
497 # Split into flag name and value
498 prev, cur = mylib.split_once(cur, '=')
499 split = 'true'
500 else:
501 split = 'false'
502 # Do NOT set 'split' without -s. Caller might not have declared it.
503 # Also does not respect var_names, because we don't need it.
504 state.BuiltinSetString(self.mem, 'split', split)
505
506 if 'cur' in var_names:
507 state.BuiltinSetString(self.mem, 'cur', cur)
508 if 'prev' in var_names:
509 state.BuiltinSetString(self.mem, 'prev', prev)
510 if 'cword' in var_names:
511 # Same weird invariant after adjustment
512 state.BuiltinSetString(self.mem, 'cword', str(n - 1))
513
514 return 0