OILS / core / completion.py View on Github | oilshell.org

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