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

1517 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 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 error
45from core import pyos
46from core import state
47from display 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 -v / 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 / compgen -A 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 d = self.mem.GetEnv()
673 for var_name in d:
674 yield var_name
675
676
677class ExternalCommandAction(CompletionAction):
678 """Complete commands in $PATH.
679
680 This is PART of compgen -A command.
681 """
682
683 def __init__(self, mem):
684 # type: (Mem) -> None
685 """
686 Args:
687 mem: for looking up Path
688 """
689 self.mem = mem
690 # Should we list everything executable in $PATH here? And then whenever
691 # $PATH is changed, regenerated it?
692 # Or we can cache directory listings? What if the contents of the dir
693 # changed?
694 # Can we look at the dir timestamp?
695 #
696 # (dir, timestamp) -> list of entries perhaps? And then every time you hit
697 # tab, do you have to check the timestamp? It should be cached by the
698 # kernel, so yes.
699 # XXX(unused?) self.ext = []
700
701 # (dir, timestamp) -> list
702 # NOTE: This cache assumes that listing a directory is slower than statting
703 # it to get the mtime. That may not be true on all systems? Either way
704 # you are reading blocks of metadata. But I guess /bin on many systems is
705 # huge, and will require lots of sys calls.
706 self.cache = {} # type: Dict[Tuple[str, int], List[str]]
707
708 def Print(self, f):
709 # type: (mylib.BufWriter) -> None
710
711 f.write('ExternalCommandAction ')
712
713 def Matches(self, comp):
714 # type: (Api) -> Iterator[str]
715 """TODO: Cache is never cleared.
716
717 - When we get a newer timestamp, we should clear the old one.
718 - When PATH is changed, we can remove old entries.
719 """
720 path_str = self.mem.env_config.Get('PATH')
721 if path_str is None:
722 # No matches if not a string
723 return
724
725 # TODO: Could be part of SearchPath?
726 path_dirs = path_str.split(':')
727 #log('path: %s', path_dirs)
728
729 executables = [] # type: List[str]
730 for d in path_dirs:
731 try:
732 key = pyos.MakeDirCacheKey(d)
733 except (IOError, OSError) as e:
734 # There could be a directory that doesn't exist in the $PATH.
735 continue
736
737 dir_exes = self.cache.get(key)
738 if dir_exes is None:
739 entries = posix.listdir(d)
740 dir_exes = []
741 for name in entries:
742 path = os_path.join(d, name)
743 # TODO: Handle exception if file gets deleted in between listing and
744 # check?
745 if not posix.access(path, X_OK):
746 continue
747 dir_exes.append(name) # append the name, not the path
748
749 self.cache[key] = dir_exes
750
751 executables.extend(dir_exes)
752
753 # TODO: Shouldn't do the prefix / space thing ourselves. readline does
754 # that at the END of the line.
755 for word in executables:
756 if word.startswith(comp.to_complete):
757 yield word
758
759
760class _Predicate(object):
761
762 def __init__(self):
763 # type: () -> None
764 pass
765
766 def Evaluate(self, candidate):
767 # type: (str) -> bool
768 raise NotImplementedError()
769
770 def Print(self, f):
771 # type: (mylib.BufWriter) -> None
772
773 f.write('???Predicate ')
774
775
776class DefaultPredicate(_Predicate):
777
778 def __init__(self):
779 # type: () -> None
780 pass
781
782 def Evaluate(self, candidate):
783 # type: (str) -> bool
784 return True
785
786 def Print(self, f):
787 # type: (mylib.BufWriter) -> None
788
789 f.write('DefaultPredicate ')
790
791
792class GlobPredicate(_Predicate):
793 """Expand into files that match a pattern. !*.py filters them.
794
795 Weird syntax:
796 -X *.py or -X !*.py
797
798 Also & is a placeholder for the string being completed?. Yeah I probably
799 want to get rid of this feature.
800 """
801
802 def __init__(self, include, glob_pat):
803 # type: (bool, str) -> None
804 self.include = include # True for inclusion, False for exclusion
805 self.glob_pat = glob_pat # extended glob syntax supported
806
807 def Evaluate(self, candidate):
808 # type: (str) -> bool
809 """Should we INCLUDE the candidate or not?"""
810 matched = libc.fnmatch(self.glob_pat, candidate)
811 # This is confusing because of bash's double-negative syntax
812 if self.include:
813 return not matched
814 else:
815 return matched
816
817 def __repr__(self):
818 # type: () -> str
819 return '<GlobPredicate %s %r>' % (self.include, self.glob_pat)
820
821 def Print(self, f):
822 # type: (mylib.BufWriter) -> None
823 f.write('GlobPredicate ')
824
825
826class UserSpec(object):
827 """Completion config for a set of commands (or complete -D -E)
828
829 - The compgen builtin exposes this DIRECTLY.
830 - Readline must call ReadlineCallback, which uses RootCompleter.
831 """
832
833 def __init__(
834 self,
835 actions, # type: List[CompletionAction]
836 extra_actions, # type: List[CompletionAction]
837 else_actions, # type: List[CompletionAction]
838 predicate, # type: _Predicate
839 prefix, # type: str
840 suffix, # type: str
841 ):
842 # type: (...) -> None
843 self.actions = actions
844 self.extra_actions = extra_actions
845 self.else_actions = else_actions
846 self.predicate = predicate # for -X
847 self.prefix = prefix
848 self.suffix = suffix
849
850 def PrintSpec(self, f):
851 # type: (mylib.BufWriter) -> None
852 """ Print with indentation of 2 """
853 f.write(' actions: ')
854 for a in self.actions:
855 a.Print(f)
856 f.write('\n')
857
858 f.write(' extra: ')
859 for a in self.extra_actions:
860 a.Print(f)
861 f.write('\n')
862
863 f.write(' else: ')
864 for a in self.else_actions:
865 a.Print(f)
866 f.write('\n')
867
868 f.write(' predicate: ')
869 self.predicate.Print(f)
870 f.write('\n')
871
872 f.write(' prefix: %s\n' % self.prefix)
873 f.write(' suffix: %s\n' % self.prefix)
874
875 def AllMatches(self, comp):
876 # type: (Api) -> Iterator[Tuple[str, comp_action_t]]
877 """yield completion candidates."""
878 num_matches = 0
879
880 for a in self.actions:
881 action_kind = a.ActionKind()
882 for match in a.Matches(comp):
883 # Special case hack to match bash for compgen -F. It doesn't filter by
884 # to_complete!
885 show = (
886 self.predicate.Evaluate(match) and
887 # ShellFuncAction results are NOT filtered by prefix!
888 (match.startswith(comp.to_complete) or
889 action_kind == comp_action_e.BashFunc))
890
891 # There are two kinds of filters: changing the string, and filtering
892 # the set of strings. So maybe have modifiers AND filters? A triple.
893 if show:
894 yield self.prefix + match + self.suffix, action_kind
895 num_matches += 1
896
897 # NOTE: extra_actions and else_actions don't respect -X, -P or -S, and we
898 # don't have to filter by startswith(comp.to_complete). They are all all
899 # FileSystemActions, which do it already.
900
901 # for -o plusdirs
902 for a in self.extra_actions:
903 for match in a.Matches(comp):
904 # We know plusdirs is a file system action
905 yield match, comp_action_e.FileSystem
906
907 # for -o default and -o dirnames
908 if num_matches == 0:
909 for a in self.else_actions:
910 for match in a.Matches(comp):
911 # both are FileSystemAction
912 yield match, comp_action_e.FileSystem
913
914 # What if the cursor is not at the end of line? See readline interface.
915 # That's OK -- we just truncate the line at the cursor?
916 # Hm actually zsh does something smarter, and which is probably preferable.
917 # It completes the word that
918
919
920# Helpers for Matches()
921def IsDollar(t):
922 # type: (Token) -> bool
923
924 # We have rules for Lit_Dollar in
925 # lex_mode_e.{ShCommand,DQ,VSub_ArgUnquoted,VSub_ArgDQ}
926 return t.id == Id.Lit_Dollar
927
928
929def IsDummy(t):
930 # type: (Token) -> bool
931 return t.id == Id.Lit_CompDummy
932
933
934def WordEndsWithCompDummy(w):
935 # type: (CompoundWord) -> bool
936 last_part = w.parts[-1]
937 UP_part = last_part
938 if last_part.tag() == word_part_e.Literal:
939 last_part = cast(Token, UP_part)
940 return last_part.id == Id.Lit_CompDummy
941 else:
942 return False
943
944
945class RootCompleter(object):
946 """Dispatch to various completers.
947
948 - Complete the OSH language (variables, etc.), or
949 - Statically evaluate argv and dispatch to a command completer.
950 """
951
952 def __init__(
953 self,
954 word_ev, # type: AbstractWordEvaluator
955 mem, # type: Mem
956 comp_lookup, # type: Lookup
957 compopt_state, # type: OptionState
958 comp_ui_state, # type: State
959 parse_ctx, # type: ParseContext
960 debug_f, # type: _DebugFile
961 ):
962 # type: (...) -> None
963 self.word_ev = word_ev # for static evaluation of words
964 self.mem = mem # to complete variable names
965 self.comp_lookup = comp_lookup
966 self.compopt_state = compopt_state # for compopt builtin
967 self.comp_ui_state = comp_ui_state
968
969 self.parse_ctx = parse_ctx
970 self.debug_f = debug_f
971
972 def Matches(self, comp):
973 # type: (Api) -> Iterator[str]
974 """
975 Args:
976 comp: Callback args from readline. Readline uses
977 set_completer_delims to tokenize the string.
978
979 Returns a list of matches relative to readline's completion_delims.
980 We have to post-process the output of various completers.
981 """
982 # Pass the original line "out of band" to the completion callback.
983 line_until_tab = comp.line[:comp.end]
984 self.comp_ui_state.line_until_tab = line_until_tab
985
986 self.parse_ctx.trail.Clear()
987 line_reader = reader.StringLineReader(line_until_tab,
988 self.parse_ctx.arena)
989 c_parser = self.parse_ctx.MakeOshParser(line_reader,
990 emit_comp_dummy=True)
991
992 # We want the output from parse_ctx, so we don't use the return value.
993 try:
994 c_parser.ParseLogicalLine()
995 except error.Parse as e:
996 # e.g. 'ls | ' will not parse. Now inspect the parser state!
997 pass
998
999 debug_f = self.debug_f
1000 trail = self.parse_ctx.trail
1001 if mylib.PYTHON:
1002 trail.PrintDebugString(debug_f)
1003
1004 #
1005 # First try completing the shell language itself.
1006 #
1007
1008 # NOTE: We get Eof_Real in the command state, but not in the middle of a
1009 # BracedVarSub. This is due to the difference between the CommandParser
1010 # and WordParser.
1011 tokens = trail.tokens
1012 last = -1
1013 if tokens[-1].id == Id.Eof_Real:
1014 last -= 1 # ignore it
1015
1016 try:
1017 t1 = tokens[last]
1018 except IndexError:
1019 t1 = None
1020 try:
1021 t2 = tokens[last - 1]
1022 except IndexError:
1023 t2 = None
1024
1025 debug_f.writeln('line: %r' % comp.line)
1026 debug_f.writeln('rl_slice from byte %d to %d: %r' %
1027 (comp.begin, comp.end, comp.line[comp.begin:comp.end]))
1028
1029 # Note: this logging crashes C++ because of type mismatch
1030 if t1:
1031 #debug_f.writeln('t1 %s' % t1)
1032 pass
1033
1034 if t2:
1035 #debug_f.writeln('t2 %s' % t2)
1036 pass
1037
1038 #debug_f.writeln('tokens %s', tokens)
1039
1040 # Each of the 'yield' statements below returns a fully-completed line, to
1041 # appease the readline library. The root cause of this dance: If there's
1042 # one candidate, readline is responsible for redrawing the input line. OSH
1043 # only displays candidates and never redraws the input line.
1044
1045 if t2: # We always have t1?
1046 # echo $
1047 if IsDollar(t2) and IsDummy(t1):
1048 self.comp_ui_state.display_pos = t2.col + 1 # 1 for $
1049 for name in self.mem.VarNames():
1050 yield line_until_tab + name # no need to quote var names
1051 return
1052
1053 # echo ${
1054 if t2.id == Id.Left_DollarBrace and IsDummy(t1):
1055 self.comp_ui_state.display_pos = t2.col + 2 # 2 for ${
1056 for name in self.mem.VarNames():
1057 # no need to quote var names
1058 yield line_until_tab + name
1059 return
1060
1061 # echo $P
1062 if t2.id == Id.VSub_DollarName and IsDummy(t1):
1063 # Example: ${undef:-$P
1064 # readline splits at ':' so we have to prepend '-$' to every completed
1065 # variable name.
1066 self.comp_ui_state.display_pos = t2.col + 1 # 1 for $
1067 # computes s[1:] for Id.VSub_DollarName
1068 to_complete = lexer.LazyStr(t2)
1069 n = len(to_complete)
1070 for name in self.mem.VarNames():
1071 if name.startswith(to_complete):
1072 # no need to quote var names
1073 yield line_until_tab + name[n:]
1074 return
1075
1076 # echo ${P
1077 if t2.id == Id.VSub_Name and IsDummy(t1):
1078 self.comp_ui_state.display_pos = t2.col # no offset
1079 to_complete = lexer.LazyStr(t2)
1080 n = len(to_complete)
1081 for name in self.mem.VarNames():
1082 if name.startswith(to_complete):
1083 # no need to quote var names
1084 yield line_until_tab + name[n:]
1085 return
1086
1087 # echo $(( VAR
1088 if t2.id == Id.Lit_ArithVarLike and IsDummy(t1):
1089 self.comp_ui_state.display_pos = t2.col # no offset
1090 to_complete = lexer.LazyStr(t2)
1091 n = len(to_complete)
1092 for name in self.mem.VarNames():
1093 if name.startswith(to_complete):
1094 # no need to quote var names
1095 yield line_until_tab + name[n:]
1096 return
1097
1098 if len(trail.words) > 0:
1099 # echo ~<TAB>
1100 # echo ~a<TAB> $(home dirs)
1101 # This must be done at a word level, and TildeDetectAll() does NOT help
1102 # here, because they don't have trailing slashes yet! We can't do it on
1103 # tokens, because otherwise f~a will complete. Looking at word_part is
1104 # EXACTLY what we want.
1105 parts = trail.words[-1].parts
1106 if len(parts) > 0 and word_.LiteralId(parts[0]) == Id.Lit_Tilde:
1107 #log('TILDE parts %s', parts)
1108
1109 if (len(parts) == 2 and
1110 word_.LiteralId(parts[1]) == Id.Lit_CompDummy):
1111 tilde_tok = cast(Token, parts[0])
1112
1113 # end of tilde
1114 self.comp_ui_state.display_pos = tilde_tok.col + 1
1115
1116 to_complete = ''
1117 for u in pyos.GetAllUsers():
1118 name = u.pw_name
1119 s = line_until_tab + ShellQuoteB(name) + '/'
1120 yield s
1121 return
1122
1123 if (len(parts) == 3 and
1124 word_.LiteralId(parts[1]) == Id.Lit_Chars and
1125 word_.LiteralId(parts[2]) == Id.Lit_CompDummy):
1126
1127 chars_tok = cast(Token, parts[1])
1128
1129 self.comp_ui_state.display_pos = chars_tok.col
1130
1131 to_complete = lexer.TokenVal(chars_tok)
1132 n = len(to_complete)
1133 for u in pyos.GetAllUsers(): # catch errors?
1134 name = u.pw_name
1135 if name.startswith(to_complete):
1136 s = line_until_tab + ShellQuoteB(name[n:]) + '/'
1137 yield s
1138 return
1139
1140 # echo hi > f<TAB> (complete redirect arg)
1141 if len(trail.redirects) > 0:
1142 r = trail.redirects[-1]
1143 # Only complete 'echo >', but not 'echo >&' or 'cat <<'
1144 # TODO: Don't complete <<< 'h'
1145 if (r.arg.tag() == redir_param_e.Word and
1146 consts.RedirArgType(r.op.id) == redir_arg_type_e.Path):
1147 arg_word = r.arg
1148 UP_word = arg_word
1149 arg_word = cast(CompoundWord, UP_word)
1150 if WordEndsWithCompDummy(arg_word):
1151 debug_f.writeln('Completing redirect arg')
1152
1153 try:
1154 val = self.word_ev.EvalWordToString(arg_word)
1155 except error.FatalRuntime as e:
1156 debug_f.writeln('Error evaluating redirect word: %s' %
1157 e)
1158 return
1159 if val.tag() != value_e.Str:
1160 debug_f.writeln("Didn't get a string from redir arg")
1161 return
1162
1163 tok = location.LeftTokenForWord(arg_word)
1164 self.comp_ui_state.display_pos = tok.col
1165
1166 comp.Update('', val.s, '', 0, [])
1167 n = len(val.s)
1168 action = FileSystemAction(False, False, True)
1169 for name in action.Matches(comp):
1170 yield line_until_tab + ShellQuoteB(name[n:])
1171 return
1172
1173 #
1174 # We're not completing the shell language. Delegate to user-defined
1175 # completion for external tools.
1176 #
1177
1178 # Set below, and set on retries.
1179 base_opts = None # type: Dict[str, bool]
1180 user_spec = None # type: Optional[UserSpec]
1181
1182 # Used on retries.
1183 partial_argv = [] # type: List[str]
1184 num_partial = -1
1185 first = None # type: str
1186
1187 if len(trail.words) > 0:
1188 # Now check if we're completing a word!
1189 if WordEndsWithCompDummy(trail.words[-1]):
1190 debug_f.writeln('Completing words')
1191 #
1192 # It didn't look like we need to complete var names, tilde, redirects,
1193 # etc. Now try partial_argv, which may involve invoking PLUGINS.
1194
1195 # needed to complete paths with ~
1196 # mycpp: workaround list cast
1197 trail_words = [cast(word_t, w) for w in trail.words]
1198 words2 = word_.TildeDetectAll(trail_words)
1199 if 0:
1200 debug_f.writeln('After tilde detection')
1201 for w in words2:
1202 print(w, file=debug_f)
1203
1204 if 0:
1205 debug_f.writeln('words2:')
1206 for w2 in words2:
1207 debug_f.writeln(' %s' % w2)
1208
1209 for w in words2:
1210 try:
1211 # TODO:
1212 # - Should we call EvalWordSequence? But turn globbing off? It
1213 # can do splitting and such.
1214 # - We could have a variant to eval TildeSub to ~ ?
1215 val = self.word_ev.EvalWordToString(w)
1216 except error.FatalRuntime:
1217 # Why would it fail?
1218 continue
1219 if val.tag() == value_e.Str:
1220 partial_argv.append(val.s)
1221 else:
1222 pass
1223
1224 debug_f.writeln('partial_argv: [%s]' % ','.join(partial_argv))
1225 num_partial = len(partial_argv)
1226
1227 first = partial_argv[0]
1228 alias_first = None # type: str
1229 if mylib.PYTHON:
1230 debug_f.writeln('alias_words: [%s]' % trail.alias_words)
1231
1232 if len(trail.alias_words) > 0:
1233 w = trail.alias_words[0]
1234 try:
1235 val = self.word_ev.EvalWordToString(w)
1236 except error.FatalRuntime:
1237 pass
1238 alias_first = val.s
1239 debug_f.writeln('alias_first: %s' % alias_first)
1240
1241 if num_partial == 0: # should never happen because of Lit_CompDummy
1242 raise AssertionError()
1243 elif num_partial == 1:
1244 base_opts, user_spec = self.comp_lookup.GetFirstSpec()
1245
1246 # Display/replace since the beginning of the first word. Note: this
1247 # is non-zero in the case of
1248 # echo $(gr and
1249 # echo `gr
1250
1251 tok = location.LeftTokenForWord(trail.words[0])
1252 self.comp_ui_state.display_pos = tok.col
1253 self.debug_f.writeln('** DISPLAY_POS = %d' %
1254 self.comp_ui_state.display_pos)
1255
1256 else:
1257 base_opts, user_spec = self.comp_lookup.GetSpecForName(
1258 first)
1259 if not user_spec and alias_first is not None:
1260 base_opts, user_spec = self.comp_lookup.GetSpecForName(
1261 alias_first)
1262 if user_spec:
1263 # Pass the aliased command to the user-defined function, and use
1264 # it for retries.
1265 first = alias_first
1266 if not user_spec:
1267 base_opts, user_spec = self.comp_lookup.GetFallback()
1268
1269 # Display since the beginning
1270 tok = location.LeftTokenForWord(trail.words[-1])
1271 self.comp_ui_state.display_pos = tok.col
1272 if mylib.PYTHON:
1273 self.debug_f.writeln('words[-1]: [%s]' %
1274 trail.words[-1])
1275
1276 self.debug_f.writeln('display_pos %d' %
1277 self.comp_ui_state.display_pos)
1278
1279 # Update the API for user-defined functions.
1280 index = len(
1281 partial_argv) - 1 # COMP_CWORD is -1 when it's empty
1282 prev = '' if index == 0 else partial_argv[index - 1]
1283 comp.Update(first, partial_argv[-1], prev, index, partial_argv)
1284
1285 # This happens in the case of [[ and ((, or a syntax error like 'echo < >'.
1286 if not user_spec:
1287 debug_f.writeln("Didn't find anything to complete")
1288 return
1289
1290 # Reset it back to what was registered. User-defined functions can mutate
1291 # it.
1292 dynamic_opts = {} # type: Dict[str, bool]
1293 self.compopt_state.dynamic_opts = dynamic_opts
1294 with ctx_Completing(self.compopt_state):
1295 done = False
1296 while not done:
1297 done = True # exhausted candidates without getting a retry
1298 try:
1299 for candidate in self._PostProcess(base_opts, dynamic_opts,
1300 user_spec, comp):
1301 yield candidate
1302 except _RetryCompletion as e:
1303 debug_f.writeln('Got 124, trying again ...')
1304 done = False
1305
1306 # Get another user_spec. The ShellFuncAction may have 'sourced' code
1307 # and run 'complete' to mutate comp_lookup, and we want to get that
1308 # new entry.
1309 if num_partial == 0:
1310 raise AssertionError()
1311 elif num_partial == 1:
1312 base_opts, user_spec = self.comp_lookup.GetFirstSpec()
1313 else:
1314 # (already processed alias_first)
1315 base_opts, user_spec = self.comp_lookup.GetSpecForName(
1316 first)
1317 if not user_spec:
1318 base_opts, user_spec = self.comp_lookup.GetFallback(
1319 )
1320
1321 def _PostProcess(
1322 self,
1323 base_opts, # type: Dict[str, bool]
1324 dynamic_opts, # type: Dict[str, bool]
1325 user_spec, # type: UserSpec
1326 comp, # type: Api
1327 ):
1328 # type: (...) -> Iterator[str]
1329 """Add trailing spaces / slashes to completion candidates, and time
1330 them.
1331
1332 NOTE: This post-processing MUST go here, and not in UserSpec, because
1333 it's in READLINE in bash. compgen doesn't see it.
1334 """
1335 self.debug_f.writeln('Completing %r ... (Ctrl-C to cancel)' %
1336 comp.line)
1337 start_time = time_.time()
1338
1339 # TODO: dedupe candidates? You can get two 'echo' in bash, which is dumb.
1340
1341 i = 0
1342 for candidate, action_kind in user_spec.AllMatches(comp):
1343 # SUBTLE: dynamic_opts is part of compopt_state, which ShellFuncAction
1344 # can mutate! So we don't want to pull this out of the loop.
1345 #
1346 # TODO: The candidates from each actions shouldn't be flattened.
1347 # for action in user_spec.Actions():
1348 # if action.IsFileSystem(): # this returns is_dir too
1349 #
1350 # action.Run() # might set dynamic opts
1351 # opt_nospace = base_opts...
1352 # if 'nospace' in dynamic_opts:
1353 # opt_nosspace = dynamic_opts['nospace']
1354 # for candidate in action.Matches():
1355 # add space or /
1356 # and do escaping too
1357 #
1358 # Or maybe you can request them on demand? Most actions are EAGER.
1359 # While the ShellacAction is LAZY? And you should be able to cancel it!
1360
1361 # NOTE: User-defined plugins (and the -P flag) can REWRITE what the user
1362 # already typed. So
1363 #
1364 # $ echo 'dir with spaces'/f<TAB>
1365 #
1366 # can be rewritten to:
1367 #
1368 # $ echo dir\ with\ spaces/foo
1369 line_until_tab = self.comp_ui_state.line_until_tab
1370 line_until_word = line_until_tab[:self.comp_ui_state.display_pos]
1371
1372 opt_filenames = base_opts.get('filenames', False)
1373 if 'filenames' in dynamic_opts:
1374 opt_filenames = dynamic_opts['filenames']
1375
1376 # compopt -o filenames is for user-defined actions. Or any
1377 # FileSystemAction needs it.
1378 if action_kind == comp_action_e.FileSystem or opt_filenames:
1379 if path_stat.isdir(candidate):
1380 s = line_until_word + ShellQuoteB(candidate) + '/'
1381 yield s
1382 continue
1383
1384 opt_nospace = base_opts.get('nospace', False)
1385 if 'nospace' in dynamic_opts:
1386 opt_nospace = dynamic_opts['nospace']
1387
1388 sp = '' if opt_nospace else ' '
1389 cand = (candidate if action_kind == comp_action_e.BashFunc else
1390 ShellQuoteB(candidate))
1391
1392 yield line_until_word + cand + sp
1393
1394 # NOTE: Can't use %.2f in production build!
1395 i += 1
1396 elapsed_ms = (time_.time() - start_time) * 1000.0
1397 plural = '' if i == 1 else 'es'
1398
1399 # TODO: Show this in the UI if it takes too long!
1400 if 0:
1401 self.debug_f.writeln(
1402 '... %d match%s for %r in %d ms (Ctrl-C to cancel)' %
1403 (i, plural, comp.line, elapsed_ms))
1404
1405 elapsed_ms = (time_.time() - start_time) * 1000.0
1406 plural = '' if i == 1 else 'es'
1407 self.debug_f.writeln('Found %d match%s for %r in %d ms' %
1408 (i, plural, comp.line, elapsed_ms))
1409
1410
1411class ReadlineCallback(object):
1412 """A callable we pass to the readline module."""
1413
1414 def __init__(self, readline, root_comp, debug_f):
1415 # type: (Optional[Readline], RootCompleter, util._DebugFile) -> None
1416 self.readline = readline
1417 self.root_comp = root_comp
1418 self.debug_f = debug_f
1419
1420 # current completion being processed
1421 if mylib.PYTHON:
1422 self.comp_iter = None # type: Iterator[str]
1423 else:
1424 self.comp_matches = None # type: List[str]
1425
1426 def _GetNextCompletion(self, state):
1427 # type: (int) -> Optional[str]
1428 if state == 0:
1429 # TODO: Tokenize it according to our language. If this is $PS2, we also
1430 # need previous lines! Could make a VirtualLineReader instead of
1431 # StringLineReader?
1432 buf = self.readline.get_line_buffer()
1433
1434 # Readline parses "words" using characters provided by
1435 # set_completer_delims().
1436 # We have our own notion of words. So let's call this a 'rl_slice'.
1437 begin = self.readline.get_begidx()
1438 end = self.readline.get_endidx()
1439
1440 comp = Api(line=buf, begin=begin, end=end)
1441 self.debug_f.writeln('Api %r %d %d' % (buf, begin, end))
1442
1443 if mylib.PYTHON:
1444 self.comp_iter = self.root_comp.Matches(comp)
1445 else:
1446 it = self.root_comp.Matches(comp)
1447 self.comp_matches = list(it)
1448 self.comp_matches.reverse()
1449
1450 if mylib.PYTHON:
1451 assert self.comp_iter is not None, self.comp_iter
1452 try:
1453 next_completion = self.comp_iter.next()
1454 except StopIteration:
1455 next_completion = None # signals the end
1456 else:
1457 assert self.comp_matches is not None, self.comp_matches
1458 try:
1459 next_completion = self.comp_matches.pop()
1460 except IndexError:
1461 next_completion = None # signals the end
1462
1463 return next_completion
1464
1465 def __call__(self, unused_word, state):
1466 # type: (str, int) -> Optional[str]
1467 """Return a single match."""
1468 try:
1469 return self._GetNextCompletion(state)
1470 except util.UserExit as e:
1471 # TODO: Could use errfmt to show this
1472 print_stderr("osh: Ignoring 'exit' in completion plugin")
1473 except error.FatalRuntime as e:
1474 # From -W. TODO: -F is swallowed now.
1475 # We should have a nicer UI for displaying errors. Maybe they shouldn't
1476 # print it to stderr. That messes up the completion display. We could
1477 # print what WOULD have been COMPREPLY here.
1478 print_stderr('osh: Runtime error while completing: %s' %
1479 e.UserErrorString())
1480 self.debug_f.writeln('Runtime error while completing: %s' %
1481 e.UserErrorString())
1482 except (IOError, OSError) as e:
1483 # test this with prlimit --nproc=1 --pid=$$
1484 print_stderr('osh: I/O error (completion): %s' %
1485 posix.strerror(e.errno))
1486 except KeyboardInterrupt:
1487 # It appears GNU readline handles Ctrl-C to cancel a long completion.
1488 # So this may never happen?
1489 print_stderr('Ctrl-C in completion')
1490 except Exception as e: # ESSENTIAL because readline swallows exceptions.
1491 if mylib.PYTHON:
1492 import traceback
1493 traceback.print_exc()
1494 print_stderr('osh: Unhandled exception while completing: %s' % e)
1495 self.debug_f.writeln('Unhandled exception while completing: %s' %
1496 e)
1497 except SystemExit as e:
1498 # I think this should no longer be called, because we don't use
1499 # sys.exit()?
1500 # But put it here in case Because readline ignores SystemExit!
1501 posix._exit(e.code)
1502
1503 return None
1504
1505
1506def ExecuteReadlineCallback(cb, word, state):
1507 # type: (ReadlineCallback, str, int) -> Optional[str]
1508 return cb.__call__(word, state)
1509
1510
1511if __name__ == '__main__':
1512 # This does basic filename copmletion
1513 import readline
1514 readline.parse_and_bind('tab: complete')
1515 while True:
1516 x = raw_input('$ ')
1517 print(x)