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

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