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