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

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