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

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