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

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