OILS / core / completion.py View on Github | oils.pub

1531 lines, 821 significant
1#!/usr/bin/env python2
2# Copyright 2016 Andy Chu. All rights reserved.
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8"""
9completion.py - Tab completion.
10
11Architecture:
12
13Completion should run in threads? For two reasons:
14
15- Completion can be slow -- e.g. completion for distributed resources
16- Because readline has a weird interface, and then you can implement
17 "iterators" in C++ or YSH. They just push onto a PIPE. Use a netstring
18 protocol and self-pipe?
19- completion can be in another process anyway?
20
21Does that mean the user code gets run in an entirely separate interpreter? The
22whole lexer/parser/cmd_eval combo has to be thread-safe. Does it get a copy of
23the same startup state?
24
25Features TODO:
26 - complete flags after alias expansion
27 - complete history expansions like zsh
28 - complete flags for all builtins, using frontend/args.py?
29 - might need a special error token
30
31bash note: most of this stuff is in pcomplete.c and bashline.c (4K lines!).
32Uses ITEMLIST with a bunch of flags.
33"""
34from __future__ import print_function
35
36import time as time_
37
38from _devbuild.gen.id_kind_asdl import Id
39from _devbuild.gen.syntax_asdl import (CompoundWord, word_part_e, word_t,
40 redir_param_e, Token)
41from _devbuild.gen.runtime_asdl import (scope_e, comp_action_e, comp_action_t)
42from _devbuild.gen.types_asdl import redir_arg_type_e
43from _devbuild.gen.value_asdl import (value, value_e)
44from core import bash_impl
45from core import error
46from core import pyos
47from core import state
48from display import ui
49from core import util
50from frontend import consts
51from frontend import lexer
52from frontend import location
53from frontend import reader
54from mycpp import mylib
55from mycpp.mylib import iteritems, log, print_stderr, tagswitch
56from osh.string_ops import ShellQuoteB
57from osh import word_
58from pylib import os_path
59from pylib import path_stat
60
61import libc
62import posix_ as posix
63from posix_ import X_OK # translated directly to C macro
64
65from typing import (Dict, Tuple, List, Iterator, Optional, Any, cast,
66 TYPE_CHECKING)
67if TYPE_CHECKING:
68 from core.comp_ui import State
69 from core.state import Mem
70 from frontend.py_readline import Readline
71 from core.util import _DebugFile
72 from frontend.parse_lib import ParseContext
73 from osh.cmd_eval import CommandEvaluator
74 from osh.split import SplitContext
75 from osh.word_eval import AbstractWordEvaluator
76
77# To quote completion candidates.
78# ! is for history expansion, which only happens interactively, but
79# completion only does too.
80# *?[] are for globs
81# {} are for brace expansion
82# ~ in filenames should be quoted
83#
84# TODO: Also escape tabs as \t and newlines at \n?
85# SHELL_META_CHARS = r' ~`!$&|;()\"*?[]{}<>' + "'"
86
87
88class _RetryCompletion(Exception):
89 """For the 'exit 124' protocol."""
90
91 def __init__(self):
92 # type: () -> None
93 pass
94
95
96# mycpp: rewrite of multiple-assignment
97# Character types
98CH_Break = 0
99CH_Other = 1
100
101# mycpp: rewrite of multiple-assignment
102# States
103ST_Begin = 0
104ST_Break = 1
105ST_Other = 2
106
107
108# State machine definition.
109# (state, char) -> (new state, emit span)
110# NOT: This would be less verbose as a dict, but a C++ compiler will turn this
111# into a lookup table anyway.
112def _TRANSITIONS(state, ch):
113 # type: (int, int) -> Tuple[int, bool]
114 if state == ST_Begin and ch == CH_Break:
115 return (ST_Break, False)
116
117 if state == ST_Begin and ch == CH_Other:
118 return (ST_Other, False)
119
120 if state == ST_Break and ch == CH_Break:
121 return (ST_Break, False)
122
123 if state == ST_Break and ch == CH_Other:
124 return (ST_Other, True)
125
126 if state == ST_Other and ch == CH_Break:
127 return (ST_Break, True)
128
129 if state == ST_Other and ch == CH_Other:
130 return (ST_Other, False)
131
132 raise ValueError("invalid (state, ch) pair")
133
134
135def AdjustArg(arg, break_chars, argv_out):
136 # type: (str, List[str], List[str]) -> None
137 # stores the end of each span
138 end_indices = [] # type: List[int]
139 state = ST_Begin
140 for i, c in enumerate(arg):
141 ch = CH_Break if c in break_chars else CH_Other
142 state, emit_span = _TRANSITIONS(state, ch)
143 if emit_span:
144 end_indices.append(i)
145
146 # Always emit a span at the end (even for empty string)
147 end_indices.append(len(arg))
148
149 begin = 0
150 for end in end_indices:
151 argv_out.append(arg[begin:end])
152 begin = end
153
154
155# NOTE: How to create temporary options? With copy.deepcopy()?
156# We might want that as a test for OVM. Copying is similar to garbage
157# collection in that you walk a graph.
158
159# These values should never be mutated.
160_DEFAULT_OPTS = {} # type: Dict[str, bool]
161
162
163class OptionState(object):
164 """Stores the compopt state of the CURRENT completion."""
165
166 def __init__(self):
167 # type: () -> None
168 # For the IN-PROGRESS completion.
169 self.currently_completing = False
170 # should be SET to a COPY of the registration options by the completer.
171 self.dynamic_opts = None # type: Dict[str, bool]
172
173
174class ctx_Completing(object):
175
176 def __init__(self, compopt_state):
177 # type: (OptionState) -> None
178 compopt_state.currently_completing = True
179 self.compopt_state = compopt_state
180
181 def __enter__(self):
182 # type: () -> None
183 pass
184
185 def __exit__(self, type, value, traceback):
186 # type: (Any, Any, Any) -> None
187 self.compopt_state.currently_completing = False
188
189
190def _PrintOpts(opts, f):
191 # type: (Dict[str, bool], mylib.BufWriter) -> None
192 f.write(' (')
193 for k, v in iteritems(opts):
194 f.write(' %s=%s' % (k, '1' if v else '0'))
195 f.write(' )\n')
196
197
198class Lookup(object):
199 """Stores completion hooks registered by the user."""
200
201 def __init__(self):
202 # type: () -> None
203
204 # Pseudo-commands __first and __fallback are for -E and -D.
205 empty_spec = UserSpec([], [], [], DefaultPredicate(), '', '')
206 do_nothing = (_DEFAULT_OPTS, empty_spec)
207 self.lookup = {
208 '__fallback': do_nothing,
209 '__first': do_nothing,
210 } # type: Dict[str, Tuple[Dict[str, bool], UserSpec]]
211
212 # for the 124 protocol
213 self.commands_with_spec_changes = [] # type: List[str]
214
215 # So you can register *.sh, unlike bash. List of (glob, [actions]),
216 # searched linearly.
217 self.patterns = [] # type: List[Tuple[str, Dict[str, bool], UserSpec]]
218
219 def __str__(self):
220 # type: () -> str
221 return '<completion.Lookup %s>' % self.lookup
222
223 def PrintSpecs(self):
224 # type: () -> None
225 """ For complete -p """
226
227 # TODO: This format could be nicer / round-trippable?
228
229 f = mylib.BufWriter()
230
231 f.write('[Commands]\n')
232 for name in sorted(self.lookup):
233 base_opts, user_spec = self.lookup[name]
234
235 f.write('%s:\n' % name)
236 _PrintOpts(base_opts, f)
237
238 user_spec.PrintSpec(f)
239
240 f.write('[Patterns]\n')
241 for pat, base_opts, spec in self.patterns:
242 #print('%s %s %s' % (pat, base_opts, spec))
243 f.write('%s:\n' % pat)
244 _PrintOpts(base_opts, f)
245
246 user_spec.PrintSpec(f)
247
248 # Print to stderr since it's not parse-able
249 print_stderr(f.getvalue())
250
251 def ClearCommandsChanged(self):
252 # type: () -> None
253 del self.commands_with_spec_changes[:]
254
255 def GetCommandsChanged(self):
256 # type: () -> List[str]
257 return self.commands_with_spec_changes
258
259 def RegisterName(self, name, base_opts, user_spec):
260 # type: (str, Dict[str, bool], UserSpec) -> None
261 """Register a completion action with a name.
262
263 Used by the 'complete' builtin.
264 """
265 self.lookup[name] = (base_opts, user_spec)
266
267 if name not in ('__fallback', '__first'):
268 self.commands_with_spec_changes.append(name)
269
270 def RegisterGlob(self, glob_pat, base_opts, user_spec):
271 # type: (str, Dict[str, bool], UserSpec) -> None
272 self.patterns.append((glob_pat, base_opts, user_spec))
273
274 def GetSpecForName(self, argv0):
275 # type: (str) -> Tuple[Dict[str, bool], UserSpec]
276 """
277 Args:
278 argv0: A finished argv0 to lookup
279 """
280 pair = self.lookup.get(argv0) # NOTE: Could be ''
281 if pair:
282 # mycpp: rewrite of tuple return
283 a, b = pair
284 return (a, b)
285
286 key = os_path.basename(argv0)
287 pair = self.lookup.get(key)
288 if pair:
289 # mycpp: rewrite of tuple return
290 a, b = pair
291 return (a, b)
292
293 for glob_pat, base_opts, user_spec in self.patterns:
294 #log('Matching %r %r', key, glob_pat)
295 if libc.fnmatch(glob_pat, key):
296 return base_opts, user_spec
297
298 return None, None
299
300 def GetFirstSpec(self):
301 # type: () -> Tuple[Dict[str, bool], UserSpec]
302 # mycpp: rewrite of tuple return
303 a, b = self.lookup['__first']
304 return (a, b)
305
306 def GetFallback(self):
307 # type: () -> Tuple[Dict[str, bool], UserSpec]
308 # mycpp: rewrite of tuple return
309 a, b = self.lookup['__fallback']
310 return (a, b)
311
312
313class Api(object):
314
315 def __init__(self, line, begin, end):
316 # type: (str, int, int) -> None
317 """
318 Args:
319 index: if -1, then we're running through compgen
320 """
321 self.line = line
322 self.begin = begin
323 self.end = end
324 self.first = None # type: Optional[str]
325 self.to_complete = None # type: Optional[str]
326 self.prev = None # type: Optional[str]
327 self.index = -1 # type: int
328 self.partial_argv = [] # type: List[str]
329 # NOTE: COMP_WORDBREAKS is initialized in Mem().
330
331 # NOTE: to_complete could be 'cur'
332 def Update(self, first, to_complete, prev, index, partial_argv):
333 # type: (str, str, str, int, List[str]) -> None
334 """Added after we've done parsing."""
335 self.first = first
336 self.to_complete = to_complete
337 self.prev = prev
338 self.index = index # COMP_CWORD
339 # COMP_ARGV and COMP_WORDS can be derived from this
340 self.partial_argv = partial_argv
341 if self.partial_argv is None:
342 self.partial_argv = []
343
344 def __repr__(self):
345 # type: () -> str
346 """For testing."""
347 return '<Api %r %d-%d>' % (self.line, self.begin, self.end)
348
349
350#
351# Actions
352#
353
354
355class CompletionAction(object):
356
357 def __init__(self):
358 # type: () -> None
359 pass
360
361 def Matches(self, comp):
362 # type: (Api) -> Iterator[str]
363 pass
364
365 def ActionKind(self):
366 # type: () -> comp_action_t
367 return comp_action_e.Other
368
369 def Print(self, f):
370 # type: (mylib.BufWriter) -> None
371 f.write('???CompletionAction ')
372
373 def __repr__(self):
374 # type: () -> str
375 return self.__class__.__name__
376
377
378class UsersAction(CompletionAction):
379 """complete -A user."""
380
381 def __init__(self):
382 # type: () -> None
383 pass
384
385 def Matches(self, comp):
386 # type: (Api) -> Iterator[str]
387 for u in pyos.GetAllUsers():
388 name = u.pw_name
389 if name.startswith(comp.to_complete):
390 yield name
391
392 def Print(self, f):
393 # type: (mylib.BufWriter) -> None
394 f.write('UserAction ')
395
396
397class TestAction(CompletionAction):
398
399 def __init__(self, words, delay=0.0):
400 # type: (List[str], Optional[float]) -> None
401 self.words = words
402 self.delay = delay
403
404 def Matches(self, comp):
405 # type: (Api) -> Iterator[str]
406 for w in self.words:
407 if w.startswith(comp.to_complete):
408 if self.delay != 0.0:
409 time_.sleep(self.delay)
410 yield w
411
412 def Print(self, f):
413 # type: (mylib.BufWriter) -> None
414 f.write('TestAction ')
415
416
417class DynamicWordsAction(CompletionAction):
418 """compgen -W '$(echo one two three)'."""
419
420 def __init__(
421 self,
422 word_ev, # type: AbstractWordEvaluator
423 splitter, # type: SplitContext
424 arg_word, # type: CompoundWord
425 errfmt, # type: ui.ErrorFormatter
426 ):
427 # type: (...) -> None
428 self.word_ev = word_ev
429 self.splitter = splitter
430 self.arg_word = arg_word
431 self.errfmt = errfmt
432
433 def Matches(self, comp):
434 # type: (Api) -> Iterator[str]
435 try:
436 val = self.word_ev.EvalWordToString(self.arg_word)
437 except error.FatalRuntime as e:
438 self.errfmt.PrettyPrintError(e)
439 raise
440
441 # SplitForWordEval() Allows \ escapes
442 candidates = self.splitter.SplitForWordEval(val.s)
443 for c in candidates:
444 if c.startswith(comp.to_complete):
445 yield c
446
447 def Print(self, f):
448 # type: (mylib.BufWriter) -> None
449 f.write('DynamicWordsAction ')
450
451
452class FileSystemAction(CompletionAction):
453 """Complete paths from the file system.
454
455 Directories will have a / suffix.
456 """
457
458 def __init__(self, dirs_only, exec_only, add_slash):
459 # type: (bool, bool, bool) -> None
460 self.dirs_only = dirs_only
461 self.exec_only = exec_only
462
463 # This is for redirects, not for UserSpec, which should respect compopt -o
464 # filenames.
465 self.add_slash = add_slash # for directories
466
467 def ActionKind(self):
468 # type: () -> comp_action_t
469 return comp_action_e.FileSystem
470
471 def Print(self, f):
472 # type: (mylib.BufWriter) -> None
473 f.write('FileSystemAction ')
474
475 def Matches(self, comp):
476 # type: (Api) -> Iterator[str]
477 to_complete = comp.to_complete
478
479 # Problem: .. and ../.. don't complete /.
480 # TODO: Set display_pos before fixing this.
481
482 #import os
483 #to_complete = os.path.normpath(to_complete)
484
485 dirname, basename = os_path.split(to_complete)
486 if dirname == '': # We're completing in this directory
487 to_list = '.'
488 else: # We're completing in some other directory
489 to_list = dirname
490
491 if 0:
492 log('basename %r' % basename)
493 log('to_list %r' % to_list)
494 log('dirname %r' % dirname)
495
496 try:
497 names = posix.listdir(to_list)
498 except (IOError, OSError) as e:
499 return # nothing
500
501 for name in names:
502 path = os_path.join(dirname, name)
503
504 if path.startswith(to_complete):
505 if self.dirs_only: # add_slash not used here
506 # NOTE: There is a duplicate isdir() check later to add a trailing
507 # slash. Consolidate the checks for fewer stat() ops. This is hard
508 # because all the completion actions must obey the same interface.
509 # We could have another type like candidate = File | Dir |
510 # OtherString ?
511 if path_stat.isdir(path):
512 yield path
513 continue
514
515 if self.exec_only:
516 # TODO: Handle exception if file gets deleted in between listing and
517 # check?
518 if not posix.access(path, X_OK):
519 continue
520
521 if self.add_slash and path_stat.isdir(path):
522 path = path + '/'
523 yield path
524 else:
525 yield path
526
527
528class CommandAction(CompletionAction):
529 """ TODO: Implement complete -C """
530
531 def __init__(self, cmd_ev, command_name):
532 # type: (CommandEvaluator, str) -> None
533 self.cmd_ev = cmd_ev
534 self.command_name = command_name
535
536 def Matches(self, comp):
537 # type: (Api) -> Iterator[str]
538 for candidate in ['TODO-complete-C']:
539 yield candidate
540
541
542class ShellFuncAction(CompletionAction):
543 """Call a user-defined function using bash's completion protocol."""
544
545 def __init__(self, cmd_ev, func, comp_lookup):
546 # type: (CommandEvaluator, value.Proc, Lookup) -> None
547 """
548 Args:
549 comp_lookup: For the 124 protocol: test if the user-defined function
550 registered a new UserSpec.
551 """
552 self.cmd_ev = cmd_ev
553 self.func = func
554 self.comp_lookup = comp_lookup
555
556 def Print(self, f):
557 # type: (mylib.BufWriter) -> None
558
559 f.write('[ShellFuncAction %s] ' % self.func.name)
560
561 def ActionKind(self):
562 # type: () -> comp_action_t
563 return comp_action_e.BashFunc
564
565 def debug(self, msg):
566 # type: (str) -> None
567 self.cmd_ev.debug_f.writeln(msg)
568
569 def Matches(self, comp):
570 # type: (Api) -> Iterator[str]
571
572 # Have to clear the response every time. TODO: Reuse the object?
573 state.SetGlobalArray(self.cmd_ev.mem, 'COMPREPLY', [])
574
575 # New completions should use COMP_ARGV, a construct specific to OSH>
576 state.SetGlobalArray(self.cmd_ev.mem, 'COMP_ARGV', comp.partial_argv)
577
578 # Old completions may use COMP_WORDS. It is split by : and = to emulate
579 # bash's behavior.
580 # More commonly, they will call _init_completion and use the 'words' output
581 # of that, ignoring COMP_WORDS.
582 comp_words = [] # type: List[str]
583 for a in comp.partial_argv:
584 AdjustArg(a, [':', '='], comp_words)
585 if comp.index == -1: # compgen
586 comp_cword = comp.index
587 else:
588 comp_cword = len(comp_words) - 1 # weird invariant
589
590 state.SetGlobalArray(self.cmd_ev.mem, 'COMP_WORDS', comp_words)
591 state.SetGlobalString(self.cmd_ev.mem, 'COMP_CWORD', str(comp_cword))
592 state.SetGlobalString(self.cmd_ev.mem, 'COMP_LINE', comp.line)
593 state.SetGlobalString(self.cmd_ev.mem, 'COMP_POINT', str(comp.end))
594
595 argv = [comp.first, comp.to_complete, comp.prev]
596 # TODO: log the arguments
597 self.debug('Running completion function %r with %d arguments' %
598 (self.func.name, len(argv)))
599
600 self.comp_lookup.ClearCommandsChanged()
601 status = self.cmd_ev.RunFuncForCompletion(self.func, argv)
602 commands_changed = self.comp_lookup.GetCommandsChanged()
603
604 self.debug('comp.first %r, commands_changed: %s' %
605 (comp.first, ', '.join(commands_changed)))
606
607 if status == 124:
608 cmd = os_path.basename(comp.first)
609 if cmd in commands_changed:
610 #self.debug('Got status 124 from %r and %s commands changed' % (self.func.name, commands_changed))
611 raise _RetryCompletion()
612 else:
613 # This happens with my own completion scripts. bash doesn't show an
614 # error.
615 self.debug(
616 "Function %r returned 124, but the completion spec for %r wasn't "
617 "changed" % (self.func.name, cmd))
618 return
619
620 # Read the response. (The name 'COMP_REPLY' would be more consistent with others.)
621 val = self.cmd_ev.mem.GetValue('COMPREPLY', scope_e.GlobalOnly)
622
623 UP_val = val
624 with tagswitch(val) as case:
625 if case(value_e.Undef):
626 # We set it above, so this error would only happen if the user
627 # unset it. Not changing it means there were no completions.
628 # TODO: This writes over the command line; it would be better
629 # to use an error object.
630 print_stderr(
631 'osh error: Ran function %r but COMPREPLY was unset' %
632 self.func.name)
633 return
634 elif case(value_e.Str):
635 val = cast(value.Str, UP_val)
636 strs = [val.s]
637 elif case(value_e.InternalStringArray):
638 val = cast(value.InternalStringArray, UP_val)
639 strs = bash_impl.InternalStringArray_GetValues(val)
640 elif case(value_e.BashArray):
641 val = cast(value.BashArray, UP_val)
642 strs = bash_impl.BashArray_GetValues(val)
643 else:
644 print_stderr(
645 'osh error: COMPREPLY should be an array or a string, got %s'
646 % ui.ValType(val))
647 return
648
649 if 0:
650 self.debug('> %r' % val) # CRASHES in C++
651
652 for s in strs:
653 #self.debug('> %r' % s)
654 yield s
655
656
657class VariablesAction(CompletionAction):
658 """compgen -v / compgen -A variable."""
659
660 def __init__(self, mem):
661 # type: (Mem) -> None
662 self.mem = mem
663
664 def Matches(self, comp):
665 # type: (Api) -> Iterator[str]
666 for var_name in self.mem.VarNames():
667 yield var_name
668
669 def Print(self, f):
670 # type: (mylib.BufWriter) -> None
671
672 f.write('VariablesAction ')
673
674
675class ExportedVarsAction(CompletionAction):
676 """compgen -e / compgen -A export."""
677
678 def __init__(self, mem):
679 # type: (Mem) -> None
680 self.mem = mem
681
682 def Matches(self, comp):
683 # type: (Api) -> Iterator[str]
684 d = self.mem.GetEnv()
685 for var_name in d:
686 yield var_name
687
688
689class ExternalCommandAction(CompletionAction):
690 """Complete commands in $PATH.
691
692 This is PART of compgen -A command.
693 """
694
695 def __init__(self, mem):
696 # type: (Mem) -> None
697 """
698 Args:
699 mem: for looking up Path
700 """
701 self.mem = mem
702 # Should we list everything executable in $PATH here? And then whenever
703 # $PATH is changed, regenerated it?
704 # Or we can cache directory listings? What if the contents of the dir
705 # changed?
706 # Can we look at the dir timestamp?
707 #
708 # (dir, timestamp) -> list of entries perhaps? And then every time you hit
709 # tab, do you have to check the timestamp? It should be cached by the
710 # kernel, so yes.
711 # XXX(unused?) self.ext = []
712
713 # (dir, timestamp) -> list
714 # NOTE: This cache assumes that listing a directory is slower than statting
715 # it to get the mtime. That may not be true on all systems? Either way
716 # you are reading blocks of metadata. But I guess /bin on many systems is
717 # huge, and will require lots of sys calls.
718 self.cache = {} # type: Dict[Tuple[str, int], List[str]]
719
720 def Print(self, f):
721 # type: (mylib.BufWriter) -> None
722
723 f.write('ExternalCommandAction ')
724
725 def Matches(self, comp):
726 # type: (Api) -> Iterator[str]
727 """TODO: Cache is never cleared.
728
729 - When we get a newer timestamp, we should clear the old one.
730 - When PATH is changed, we can remove old entries.
731 """
732 path_str = self.mem.env_config.Get('PATH')
733 if path_str is None:
734 # No matches if not a string
735 return
736
737 # TODO: Could be part of SearchPath?
738 path_dirs = path_str.split(':')
739 #log('path: %s', path_dirs)
740
741 executables = [] # type: List[str]
742 for d in path_dirs:
743 try:
744 key = pyos.MakeDirCacheKey(d)
745 except (IOError, OSError) as e:
746 # There could be a directory that doesn't exist in the $PATH.
747 continue
748
749 dir_exes = self.cache.get(key)
750 if dir_exes is None:
751 entries = posix.listdir(d)
752 dir_exes = []
753 for name in entries:
754 path = os_path.join(d, name)
755 # TODO: Handle exception if file gets deleted in between listing and
756 # check?
757 if not posix.access(path, X_OK):
758 continue
759 dir_exes.append(name) # append the name, not the path
760
761 self.cache[key] = dir_exes
762
763 executables.extend(dir_exes)
764
765 # TODO: Shouldn't do the prefix / space thing ourselves. readline does
766 # that at the END of the line.
767 for word in executables:
768 if word.startswith(comp.to_complete):
769 yield word
770
771
772class _Predicate(object):
773
774 def __init__(self):
775 # type: () -> None
776 pass
777
778 def Evaluate(self, candidate):
779 # type: (str) -> bool
780 raise NotImplementedError()
781
782 def Print(self, f):
783 # type: (mylib.BufWriter) -> None
784
785 f.write('???Predicate ')
786
787
788class DefaultPredicate(_Predicate):
789
790 def __init__(self):
791 # type: () -> None
792 pass
793
794 def Evaluate(self, candidate):
795 # type: (str) -> bool
796 return True
797
798 def Print(self, f):
799 # type: (mylib.BufWriter) -> None
800
801 f.write('DefaultPredicate ')
802
803
804class GlobPredicate(_Predicate):
805 """Expand into files that match a pattern. !*.py filters them.
806
807 Weird syntax:
808 -X *.py or -X !*.py
809
810 Also & is a placeholder for the string being completed?. Yeah I probably
811 want to get rid of this feature.
812 """
813
814 def __init__(self, include, glob_pat):
815 # type: (bool, str) -> None
816 self.include = include # True for inclusion, False for exclusion
817 self.glob_pat = glob_pat # extended glob syntax supported
818
819 def Evaluate(self, candidate):
820 # type: (str) -> bool
821 """Should we INCLUDE the candidate or not?"""
822 matched = libc.fnmatch(self.glob_pat, candidate)
823 # This is confusing because of bash's double-negative syntax
824 if self.include:
825 return not matched
826 else:
827 return matched
828
829 def __repr__(self):
830 # type: () -> str
831 return '<GlobPredicate %s %r>' % (self.include, self.glob_pat)
832
833 def Print(self, f):
834 # type: (mylib.BufWriter) -> None
835 f.write('GlobPredicate ')
836
837
838class UserSpec(object):
839 """Completion config for a set of commands (or complete -D -E)
840
841 - The compgen builtin exposes this DIRECTLY.
842 - Readline must call ReadlineCallback, which uses RootCompleter.
843 """
844
845 def __init__(
846 self,
847 actions, # type: List[CompletionAction]
848 extra_actions, # type: List[CompletionAction]
849 else_actions, # type: List[CompletionAction]
850 predicate, # type: _Predicate
851 prefix, # type: str
852 suffix, # type: str
853 ):
854 # type: (...) -> None
855 self.actions = actions
856 self.extra_actions = extra_actions
857 self.else_actions = else_actions
858 self.predicate = predicate # for -X
859 self.prefix = prefix
860 self.suffix = suffix
861
862 def PrintSpec(self, f):
863 # type: (mylib.BufWriter) -> None
864 """ Print with indentation of 2 """
865 f.write(' actions: ')
866 for a in self.actions:
867 a.Print(f)
868 f.write('\n')
869
870 f.write(' extra: ')
871 for a in self.extra_actions:
872 a.Print(f)
873 f.write('\n')
874
875 f.write(' else: ')
876 for a in self.else_actions:
877 a.Print(f)
878 f.write('\n')
879
880 f.write(' predicate: ')
881 self.predicate.Print(f)
882 f.write('\n')
883
884 f.write(' prefix: %s\n' % self.prefix)
885 f.write(' suffix: %s\n' % self.prefix)
886
887 def AllMatches(self, comp):
888 # type: (Api) -> Iterator[Tuple[str, comp_action_t]]
889 """yield completion candidates."""
890 num_matches = 0
891
892 for a in self.actions:
893 action_kind = a.ActionKind()
894 for match in a.Matches(comp):
895 # Special case hack to match bash for compgen -F. It doesn't filter by
896 # to_complete!
897 show = (
898 self.predicate.Evaluate(match) and
899 # ShellFuncAction results are NOT filtered by prefix!
900 (match.startswith(comp.to_complete) or
901 action_kind == comp_action_e.BashFunc))
902
903 # There are two kinds of filters: changing the string, and filtering
904 # the set of strings. So maybe have modifiers AND filters? A triple.
905 if show:
906 yield self.prefix + match + self.suffix, action_kind
907 num_matches += 1
908
909 # NOTE: extra_actions and else_actions don't respect -X, -P or -S, and we
910 # don't have to filter by startswith(comp.to_complete). They are all all
911 # FileSystemActions, which do it already.
912
913 # for -o plusdirs
914 for a in self.extra_actions:
915 for match in a.Matches(comp):
916 # We know plusdirs is a file system action
917 yield match, comp_action_e.FileSystem
918
919 # for -o default and -o dirnames
920 if num_matches == 0:
921 for a in self.else_actions:
922 for match in a.Matches(comp):
923 # both are FileSystemAction
924 yield match, comp_action_e.FileSystem
925
926 # What if the cursor is not at the end of line? See readline interface.
927 # That's OK -- we just truncate the line at the cursor?
928 # Hm actually zsh does something smarter, and which is probably preferable.
929 # It completes the word that
930
931
932# Helpers for Matches()
933def IsDollar(t):
934 # type: (Token) -> bool
935
936 # We have rules for Lit_Dollar in
937 # lex_mode_e.{ShCommand,DQ,VSub_ArgUnquoted,VSub_ArgDQ}
938 return t.id == Id.Lit_Dollar
939
940
941def IsDummy(t):
942 # type: (Token) -> bool
943 return t.id == Id.Lit_CompDummy
944
945
946def WordEndsWithCompDummy(w):
947 # type: (CompoundWord) -> bool
948 last_part = w.parts[-1]
949 UP_part = last_part
950 if last_part.tag() == word_part_e.Literal:
951 last_part = cast(Token, UP_part)
952 return last_part.id == Id.Lit_CompDummy
953 else:
954 return False
955
956
957class RootCompleter(object):
958 """Dispatch to various completers.
959
960 - Complete the OSH language (variables, etc.), or
961 - Statically evaluate argv and dispatch to a command completer.
962 """
963
964 def __init__(
965 self,
966 word_ev, # type: AbstractWordEvaluator
967 mem, # type: Mem
968 comp_lookup, # type: Lookup
969 compopt_state, # type: OptionState
970 comp_ui_state, # type: State
971 parse_ctx, # type: ParseContext
972 debug_f, # type: _DebugFile
973 ):
974 # type: (...) -> None
975 self.word_ev = word_ev # for static evaluation of words
976 self.mem = mem # to complete variable names
977 self.comp_lookup = comp_lookup
978 self.compopt_state = compopt_state # for compopt builtin
979 self.comp_ui_state = comp_ui_state
980
981 self.parse_ctx = parse_ctx
982 self.debug_f = debug_f
983
984 def Matches(self, comp):
985 # type: (Api) -> Iterator[str]
986 """
987 Args:
988 comp: Callback args from readline. Readline uses
989 set_completer_delims to tokenize the string.
990
991 Returns a list of matches relative to readline's completion_delims.
992 We have to post-process the output of various completers.
993 """
994 # Pass the original line "out of band" to the completion callback.
995 line_until_tab = comp.line[:comp.end]
996 self.comp_ui_state.line_until_tab = line_until_tab
997
998 self.parse_ctx.trail.Clear()
999 line_reader = reader.StringLineReader(line_until_tab,
1000 self.parse_ctx.arena)
1001 c_parser = self.parse_ctx.MakeOshParser(
1002 line_reader,
1003 self.mem.exec_opts.interactive(),
1004 emit_comp_dummy=True)
1005
1006 # We want the output from parse_ctx, so we don't use the return value.
1007 try:
1008 c_parser.ParseLogicalLine()
1009 except error.Parse as e:
1010 # e.g. 'ls | ' will not parse. Now inspect the parser state!
1011 pass
1012
1013 debug_f = self.debug_f
1014 trail = self.parse_ctx.trail
1015 if mylib.PYTHON:
1016 trail.PrintDebugString(debug_f)
1017
1018 #
1019 # First try completing the shell language itself.
1020 #
1021
1022 # NOTE: We get Eof_Real in the command state, but not in the middle of a
1023 # BracedVarSub. This is due to the difference between the CommandParser
1024 # and WordParser.
1025 tokens = trail.tokens
1026 last = -1
1027 if tokens[-1].id == Id.Eof_Real:
1028 last -= 1 # ignore it
1029
1030 try:
1031 t1 = tokens[last]
1032 except IndexError:
1033 t1 = None
1034 try:
1035 t2 = tokens[last - 1]
1036 except IndexError:
1037 t2 = None
1038
1039 debug_f.writeln('line: %r' % comp.line)
1040 debug_f.writeln('rl_slice from byte %d to %d: %r' %
1041 (comp.begin, comp.end, comp.line[comp.begin:comp.end]))
1042
1043 # Note: this logging crashes C++ because of type mismatch
1044 if t1:
1045 #debug_f.writeln('t1 %s' % t1)
1046 pass
1047
1048 if t2:
1049 #debug_f.writeln('t2 %s' % t2)
1050 pass
1051
1052 #debug_f.writeln('tokens %s', tokens)
1053
1054 # Each of the 'yield' statements below returns a fully-completed line, to
1055 # appease the readline library. The root cause of this dance: If there's
1056 # one candidate, readline is responsible for redrawing the input line. OSH
1057 # only displays candidates and never redraws the input line.
1058
1059 if t2: # We always have t1?
1060 # echo $
1061 if IsDollar(t2) and IsDummy(t1):
1062 self.comp_ui_state.display_pos = t2.col + 1 # 1 for $
1063 for name in self.mem.VarNames():
1064 yield line_until_tab + name # no need to quote var names
1065 return
1066
1067 # echo ${
1068 if t2.id == Id.Left_DollarBrace and IsDummy(t1):
1069 self.comp_ui_state.display_pos = t2.col + 2 # 2 for ${
1070 for name in self.mem.VarNames():
1071 # no need to quote var names
1072 yield line_until_tab + name
1073 return
1074
1075 # echo $P
1076 if t2.id == Id.VSub_DollarName and IsDummy(t1):
1077 # Example: ${undef:-$P
1078 # readline splits at ':' so we have to prepend '-$' to every completed
1079 # variable name.
1080 self.comp_ui_state.display_pos = t2.col + 1 # 1 for $
1081 # computes s[1:] for Id.VSub_DollarName
1082 to_complete = lexer.LazyStr(t2)
1083 n = len(to_complete)
1084 for name in self.mem.VarNames():
1085 if name.startswith(to_complete):
1086 # no need to quote var names
1087 yield line_until_tab + name[n:]
1088 return
1089
1090 # echo ${P
1091 if t2.id == Id.VSub_Name and IsDummy(t1):
1092 self.comp_ui_state.display_pos = t2.col # no offset
1093 to_complete = lexer.LazyStr(t2)
1094 n = len(to_complete)
1095 for name in self.mem.VarNames():
1096 if name.startswith(to_complete):
1097 # no need to quote var names
1098 yield line_until_tab + name[n:]
1099 return
1100
1101 # echo $(( VAR
1102 if t2.id == Id.Lit_ArithVarLike and IsDummy(t1):
1103 self.comp_ui_state.display_pos = t2.col # no offset
1104 to_complete = lexer.LazyStr(t2)
1105 n = len(to_complete)
1106 for name in self.mem.VarNames():
1107 if name.startswith(to_complete):
1108 # no need to quote var names
1109 yield line_until_tab + name[n:]
1110 return
1111
1112 if len(trail.words) > 0:
1113 # echo ~<TAB>
1114 # echo ~a<TAB> $(home dirs)
1115 # This must be done at a word level, and TildeDetectAll() does NOT help
1116 # here, because they don't have trailing slashes yet! We can't do it on
1117 # tokens, because otherwise f~a will complete. Looking at word_part is
1118 # EXACTLY what we want.
1119 parts = trail.words[-1].parts
1120 if len(parts) > 0 and word_.LiteralId(parts[0]) == Id.Lit_Tilde:
1121 #log('TILDE parts %s', parts)
1122
1123 if (len(parts) == 2 and
1124 word_.LiteralId(parts[1]) == Id.Lit_CompDummy):
1125 tilde_tok = cast(Token, parts[0])
1126
1127 # end of tilde
1128 self.comp_ui_state.display_pos = tilde_tok.col + 1
1129
1130 to_complete = ''
1131 for u in pyos.GetAllUsers():
1132 name = u.pw_name
1133 s = line_until_tab + ShellQuoteB(name) + '/'
1134 yield s
1135 return
1136
1137 if (len(parts) == 3 and
1138 word_.LiteralId(parts[1]) == Id.Lit_Chars and
1139 word_.LiteralId(parts[2]) == Id.Lit_CompDummy):
1140
1141 chars_tok = cast(Token, parts[1])
1142
1143 self.comp_ui_state.display_pos = chars_tok.col
1144
1145 to_complete = lexer.TokenVal(chars_tok)
1146 n = len(to_complete)
1147 for u in pyos.GetAllUsers(): # catch errors?
1148 name = u.pw_name
1149 if name.startswith(to_complete):
1150 s = line_until_tab + ShellQuoteB(name[n:]) + '/'
1151 yield s
1152 return
1153
1154 # echo hi > f<TAB> (complete redirect arg)
1155 if len(trail.redirects) > 0:
1156 r = trail.redirects[-1]
1157 # Only complete 'echo >', but not 'echo >&' or 'cat <<'
1158 # TODO: Don't complete <<< 'h'
1159 if (r.arg.tag() == redir_param_e.Word and
1160 consts.RedirArgType(r.op.id) == redir_arg_type_e.Path):
1161 arg_word = r.arg
1162 UP_word = arg_word
1163 arg_word = cast(CompoundWord, UP_word)
1164 if WordEndsWithCompDummy(arg_word):
1165 debug_f.writeln('Completing redirect arg')
1166
1167 try:
1168 val = self.word_ev.EvalWordToString(arg_word)
1169 except error.FatalRuntime as e:
1170 debug_f.writeln('Error evaluating redirect word: %s' %
1171 e)
1172 return
1173 if val.tag() != value_e.Str:
1174 debug_f.writeln("Didn't get a string from redir arg")
1175 return
1176
1177 tok = location.LeftTokenForWord(arg_word)
1178 self.comp_ui_state.display_pos = tok.col
1179
1180 comp.Update('', val.s, '', 0, [])
1181 n = len(val.s)
1182 action = FileSystemAction(False, False, True)
1183 for name in action.Matches(comp):
1184 yield line_until_tab + ShellQuoteB(name[n:])
1185 return
1186
1187 #
1188 # We're not completing the shell language. Delegate to user-defined
1189 # completion for external tools.
1190 #
1191
1192 # Set below, and set on retries.
1193 base_opts = None # type: Dict[str, bool]
1194 user_spec = None # type: Optional[UserSpec]
1195
1196 # Used on retries.
1197 partial_argv = [] # type: List[str]
1198 num_partial = -1
1199 first = None # type: Optional[str]
1200
1201 if len(trail.words) > 0:
1202 # Now check if we're completing a word!
1203 if WordEndsWithCompDummy(trail.words[-1]):
1204 debug_f.writeln('Completing words')
1205 #
1206 # It didn't look like we need to complete var names, tilde, redirects,
1207 # etc. Now try partial_argv, which may involve invoking PLUGINS.
1208
1209 # needed to complete paths with ~
1210 # mycpp: workaround list cast
1211 trail_words = [cast(word_t, w) for w in trail.words]
1212 words2 = word_.TildeDetectAll(trail_words)
1213 if 0:
1214 debug_f.writeln('After tilde detection')
1215 for w in words2:
1216 print(w, file=debug_f)
1217
1218 if 0:
1219 debug_f.writeln('words2:')
1220 for w2 in words2:
1221 debug_f.writeln(' %s' % w2)
1222
1223 for w in words2:
1224 try:
1225 # TODO:
1226 # - Should we call EvalWordSequence? But turn globbing off? It
1227 # can do splitting and such.
1228 # - We could have a variant to eval TildeSub to ~ ?
1229 val = self.word_ev.EvalWordToString(w)
1230 except error.FatalRuntime:
1231 # Why would it fail?
1232 continue
1233 if val.tag() == value_e.Str:
1234 partial_argv.append(val.s)
1235 else:
1236 pass
1237
1238 debug_f.writeln('partial_argv: [%s]' % ','.join(partial_argv))
1239 num_partial = len(partial_argv)
1240
1241 first = partial_argv[0]
1242 alias_first = None # type: Optional[str]
1243 if mylib.PYTHON:
1244 debug_f.writeln('alias_words: [%s]' % trail.alias_words)
1245
1246 if len(trail.alias_words) > 0:
1247 w = trail.alias_words[0]
1248 try:
1249 val = self.word_ev.EvalWordToString(w)
1250 except error.FatalRuntime:
1251 pass
1252 alias_first = val.s
1253 debug_f.writeln('alias_first: %s' % alias_first)
1254
1255 if num_partial == 0: # should never happen because of Lit_CompDummy
1256 raise AssertionError()
1257 elif num_partial == 1:
1258 base_opts, user_spec = self.comp_lookup.GetFirstSpec()
1259
1260 # Display/replace since the beginning of the first word. Note: this
1261 # is non-zero in the case of
1262 # echo $(gr and
1263 # echo `gr
1264
1265 tok = location.LeftTokenForWord(trail.words[0])
1266 self.comp_ui_state.display_pos = tok.col
1267 self.debug_f.writeln('** DISPLAY_POS = %d' %
1268 self.comp_ui_state.display_pos)
1269
1270 else:
1271 base_opts, user_spec = self.comp_lookup.GetSpecForName(
1272 first)
1273 if not user_spec and alias_first is not None:
1274 base_opts, user_spec = self.comp_lookup.GetSpecForName(
1275 alias_first)
1276 if user_spec:
1277 # Pass the aliased command to the user-defined function, and use
1278 # it for retries.
1279 first = alias_first
1280 if not user_spec:
1281 base_opts, user_spec = self.comp_lookup.GetFallback()
1282
1283 # Display since the beginning
1284 tok = location.LeftTokenForWord(trail.words[-1])
1285 self.comp_ui_state.display_pos = tok.col
1286 if mylib.PYTHON:
1287 self.debug_f.writeln('words[-1]: [%s]' %
1288 trail.words[-1])
1289
1290 self.debug_f.writeln('display_pos %d' %
1291 self.comp_ui_state.display_pos)
1292
1293 # Update the API for user-defined functions.
1294 index = len(
1295 partial_argv) - 1 # COMP_CWORD is -1 when it's empty
1296 prev = '' if index == 0 else partial_argv[index - 1]
1297 comp.Update(first, partial_argv[-1], prev, index, partial_argv)
1298
1299 # This happens in the case of [[ and ((, or a syntax error like 'echo < >'.
1300 if not user_spec:
1301 debug_f.writeln("Didn't find anything to complete")
1302 return
1303
1304 # Reset it back to what was registered. User-defined functions can mutate
1305 # it.
1306 dynamic_opts = {} # type: Dict[str, bool]
1307 self.compopt_state.dynamic_opts = dynamic_opts
1308 with ctx_Completing(self.compopt_state):
1309 done = False
1310 while not done:
1311 done = True # exhausted candidates without getting a retry
1312 try:
1313 for candidate in self._PostProcess(base_opts, dynamic_opts,
1314 user_spec, comp):
1315 yield candidate
1316 except _RetryCompletion as e:
1317 debug_f.writeln('Got 124, trying again ...')
1318 done = False
1319
1320 # Get another user_spec. The ShellFuncAction may have 'sourced' code
1321 # and run 'complete' to mutate comp_lookup, and we want to get that
1322 # new entry.
1323 if num_partial == 0:
1324 raise AssertionError()
1325 elif num_partial == 1:
1326 base_opts, user_spec = self.comp_lookup.GetFirstSpec()
1327 else:
1328 # (already processed alias_first)
1329 base_opts, user_spec = self.comp_lookup.GetSpecForName(
1330 first)
1331 if not user_spec:
1332 base_opts, user_spec = self.comp_lookup.GetFallback(
1333 )
1334
1335 def _PostProcess(
1336 self,
1337 base_opts, # type: Dict[str, bool]
1338 dynamic_opts, # type: Dict[str, bool]
1339 user_spec, # type: UserSpec
1340 comp, # type: Api
1341 ):
1342 # type: (...) -> Iterator[str]
1343 """Add trailing spaces / slashes to completion candidates, and time
1344 them.
1345
1346 NOTE: This post-processing MUST go here, and not in UserSpec, because
1347 it's in READLINE in bash. compgen doesn't see it.
1348 """
1349 self.debug_f.writeln('Completing %r ... (Ctrl-C to cancel)' %
1350 comp.line)
1351 start_time = time_.time()
1352
1353 # TODO: dedupe candidates? You can get two 'echo' in bash, which is dumb.
1354
1355 i = 0
1356 for candidate, action_kind in user_spec.AllMatches(comp):
1357 # SUBTLE: dynamic_opts is part of compopt_state, which ShellFuncAction
1358 # can mutate! So we don't want to pull this out of the loop.
1359 #
1360 # TODO: The candidates from each actions shouldn't be flattened.
1361 # for action in user_spec.Actions():
1362 # if action.IsFileSystem(): # this returns is_dir too
1363 #
1364 # action.Run() # might set dynamic opts
1365 # opt_nospace = base_opts...
1366 # if 'nospace' in dynamic_opts:
1367 # opt_nosspace = dynamic_opts['nospace']
1368 # for candidate in action.Matches():
1369 # add space or /
1370 # and do escaping too
1371 #
1372 # Or maybe you can request them on demand? Most actions are EAGER.
1373 # While the ShellacAction is LAZY? And you should be able to cancel it!
1374
1375 # NOTE: User-defined plugins (and the -P flag) can REWRITE what the user
1376 # already typed. So
1377 #
1378 # $ echo 'dir with spaces'/f<TAB>
1379 #
1380 # can be rewritten to:
1381 #
1382 # $ echo dir\ with\ spaces/foo
1383 line_until_tab = self.comp_ui_state.line_until_tab
1384 line_until_word = line_until_tab[:self.comp_ui_state.display_pos]
1385
1386 opt_filenames = base_opts.get('filenames', False)
1387 if 'filenames' in dynamic_opts:
1388 opt_filenames = dynamic_opts['filenames']
1389
1390 # compopt -o filenames is for user-defined actions. Or any
1391 # FileSystemAction needs it.
1392 if action_kind == comp_action_e.FileSystem or opt_filenames:
1393 if path_stat.isdir(candidate):
1394 s = line_until_word + ShellQuoteB(candidate) + '/'
1395 yield s
1396 continue
1397
1398 opt_nospace = base_opts.get('nospace', False)
1399 if 'nospace' in dynamic_opts:
1400 opt_nospace = dynamic_opts['nospace']
1401
1402 sp = '' if opt_nospace else ' '
1403 cand = (candidate if action_kind == comp_action_e.BashFunc else
1404 ShellQuoteB(candidate))
1405
1406 yield line_until_word + cand + sp
1407
1408 # NOTE: Can't use %.2f in production build!
1409 i += 1
1410 elapsed_ms = (time_.time() - start_time) * 1000.0
1411 plural = '' if i == 1 else 'es'
1412
1413 # TODO: Show this in the UI if it takes too long!
1414 if 0:
1415 self.debug_f.writeln(
1416 '... %d match%s for %r in %d ms (Ctrl-C to cancel)' %
1417 (i, plural, comp.line, elapsed_ms))
1418
1419 elapsed_ms = (time_.time() - start_time) * 1000.0
1420 plural = '' if i == 1 else 'es'
1421 self.debug_f.writeln('Found %d match%s for %r in %d ms' %
1422 (i, plural, comp.line, elapsed_ms))
1423
1424
1425class ReadlineCallback(object):
1426 """A callable we pass to the readline module."""
1427
1428 def __init__(self, readline, root_comp, debug_f):
1429 # type: (Optional[Readline], RootCompleter, util._DebugFile) -> None
1430 self.readline = readline
1431 self.root_comp = root_comp
1432 self.debug_f = debug_f
1433
1434 # current completion being processed
1435 if mylib.PYTHON:
1436 self.comp_iter = None # type: Iterator[str]
1437 else:
1438 self.comp_matches = None # type: List[str]
1439
1440 def _GetNextCompletion(self, state):
1441 # type: (int) -> Optional[str]
1442 if state == 0:
1443 # TODO: Tokenize it according to our language. If this is $PS2, we also
1444 # need previous lines! Could make a VirtualLineReader instead of
1445 # StringLineReader?
1446 buf = self.readline.get_line_buffer()
1447
1448 # Readline parses "words" using characters provided by
1449 # set_completer_delims().
1450 # We have our own notion of words. So let's call this a 'rl_slice'.
1451 begin = self.readline.get_begidx()
1452 end = self.readline.get_endidx()
1453
1454 comp = Api(line=buf, begin=begin, end=end)
1455 self.debug_f.writeln('Api %r %d %d' % (buf, begin, end))
1456
1457 if mylib.PYTHON:
1458 self.comp_iter = self.root_comp.Matches(comp)
1459 else:
1460 it = self.root_comp.Matches(comp)
1461 self.comp_matches = list(it)
1462 self.comp_matches.reverse()
1463
1464 if mylib.PYTHON:
1465 assert self.comp_iter is not None, self.comp_iter
1466 try:
1467 next_completion = self.comp_iter.next()
1468 except StopIteration:
1469 next_completion = None # signals the end
1470 else:
1471 assert self.comp_matches is not None, self.comp_matches
1472 try:
1473 next_completion = self.comp_matches.pop()
1474 except IndexError:
1475 next_completion = None # signals the end
1476
1477 return next_completion
1478
1479 def __call__(self, unused_word, state):
1480 # type: (str, int) -> Optional[str]
1481 """Return a single match."""
1482 try:
1483 return self._GetNextCompletion(state)
1484 except util.HardExit as e:
1485 # TODO: Could use errfmt to show this
1486 print_stderr("oils: Ignoring 'exit' in completion plugin")
1487 except error.FatalRuntime as e:
1488 # From -W. TODO: -F is swallowed now.
1489 # We should have a nicer UI for displaying errors. Maybe they shouldn't
1490 # print it to stderr. That messes up the completion display. We could
1491 # print what WOULD have been COMPREPLY here.
1492 print_stderr('oils: Runtime error while completing: %s' %
1493 e.UserErrorString())
1494 self.debug_f.writeln('Runtime error while completing: %s' %
1495 e.UserErrorString())
1496 except (IOError, OSError) as e:
1497 # test this with prlimit --nproc=1 --pid=$$
1498 print_stderr('oils: I/O error (completion): %s' %
1499 posix.strerror(e.errno))
1500 except KeyboardInterrupt:
1501 # It appears GNU readline handles Ctrl-C to cancel a long completion.
1502 # So this may never happen?
1503 print_stderr('Ctrl-C in completion')
1504 except Exception as e: # ESSENTIAL because readline swallows exceptions.
1505 if mylib.PYTHON:
1506 import traceback
1507 traceback.print_exc()
1508 print_stderr('oils: Unhandled exception while completing: %s' % e)
1509 self.debug_f.writeln('Unhandled exception while completing: %s' %
1510 e)
1511 except SystemExit as e:
1512 # I think this should no longer be called, because we don't use
1513 # sys.exit()?
1514 # But put it here in case Because readline ignores SystemExit!
1515 posix._exit(e.code)
1516
1517 return None
1518
1519
1520def ExecuteReadlineCallback(cb, word, state):
1521 # type: (ReadlineCallback, str, int) -> Optional[str]
1522 return cb.__call__(word, state)
1523
1524
1525if __name__ == '__main__':
1526 # This does basic filename copmletion
1527 import readline
1528 readline.parse_and_bind('tab: complete')
1529 while True:
1530 x = raw_input('$ ')
1531 print(x)