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

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